ECE366 - Lesson 8

Putting Everything Together

Instructor: Professor Hong

Review & Questions

## 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 ```
## 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>; </> ); } ```
## Putting it all together
## setup.sql ``` CREATE DATABASE rps; GRANT ALL PRIVILEGES ON DATABASE rps TO postgres; \c rps \i /docker-entrypoint-initdb.d/1_player.sql \i /docker-entrypoint-initdb.d/2_game.sql \i /docker-entrypoint-initdb.d/3_game_round.sql \i /docker-entrypoint-initdb.d/4_sample_records.sql ```
## player.sql ``` CREATE SEQUENCE player_seq start with 1; CREATE TABLE player ( player_id bigint NOT NULL DEFAULT nextval('player_seq'), user_name varchar(50) NOT NULL UNIQUE, password varchar(50) NOT NULL, total_games int DEFAULT 0, total_wins int DEFAULT 0, total_losses int DEFAULT 0, PRIMARY KEY (player_id) ); ```
## game.sql ``` CREATE SEQUENCE game_seq start with 1; CREATE TABLE game ( game_id bigint NOT NULL DEFAULT nextval('game_seq'), p1_id bigint NOT NULL, p2_id bigint NOT NULL, p1_score int NOT NULL DEFAULT 0, p2_score int NOT NULL DEFAULT 0, total_rounds int NOT NULL, current_round int NOT NULL, winner_id bigint DEFAULT NULL, PRIMARY KEY (game_id), FOREIGN KEY (p1_id) REFERENCES player(player_id) ON DELETE CASCADE, FOREIGN KEY (p2_id) REFERENCES player(player_id) ON DELETE CASCADE, CHECK (p1_id <> p2_id), CHECK (winner_id = p1_id OR winner_id = p2_id) ); ```
## game_round.sql ``` CREATE TABLE game_round ( game_id bigint NOT NULL, round_number int NOT NULL, p1_choice varchar(50) NOT NULL, p2_choice varchar(50) NOT NULL, PRIMARY KEY (game_id, round_number), FOREIGN KEY (game_id) REFERENCES game(game_id) ON DELETE CASCADE ); ```
## Initializing database Dockerfile ``` FROM postgres # Copy the SQL files to the container COPY setup.sql /docker-entrypoint-initdb.d/0_setup.sql COPY player.sql /docker-entrypoint-initdb.d/1_player.sql COPY game.sql /docker-entrypoint-initdb.d/2_game.sql COPY game_round.sql /docker-entrypoint-initdb.d/3_game_round.sql COPY sample_records.sql /docker-entrypoint-initdb.d/4_sample_records.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/rpsjdbc-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/", ``` Change url in app.js to ```/api/player/1```
## 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: db environment: - POSTGRES_DB=postgres - POSTGRES_PASSWORD=password expose: - 5432:5432 ports: - 5432:5432 restart: always app: build: server/rpsjdbc expose: - 8080:8080 ports: - 8080:8080 environment: - POSTGRES_DB=postgres - POSTGRES_PASSWORD=password depends_on: - db ui: build: client/rps 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"); } } ```
## Index.html src/main/resources/static/index.html ``` <!DOCTYPE html> <html> <head> <title>Hello WebSocket</title> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"> <link href="/main.css" rel="stylesheet"> <script src="https://code.jquery.com/jquery-3.1.1.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/@stomp/stompjs@7.0.0/bundles/stomp.umd.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 const stompClient = new StompJs.Client({ brokerURL: 'ws://localhost:8080/gs-guide-websocket' }); stompClient.onConnect = (frame) => { setConnected(true); console.log('Connected: ' + frame); stompClient.subscribe('/topic/greetings', (greeting) => { showGreeting(JSON.parse(greeting.body).content); }); }; stompClient.onWebSocketError = (error) => { console.error('Error with websocket', error); }; stompClient.onStompError = (frame) => { console.error('Broker reported error: ' + frame.headers['message']); console.error('Additional details: ' + frame.body); }; function setConnected(connected) { $("#connect").prop("disabled", connected); $("#disconnect").prop("disabled", !connected); if (connected) { $("#conversation").show(); } else { $("#conversation").hide(); } $("#greetings").html(""); } function connect() { stompClient.activate(); } function disconnect() { stompClient.deactivate(); setConnected(false); console.log("Disconnected"); } function sendName() { stompClient.publish({ destination: "/app/hello", body: JSON.stringify({'name': $("#name").val()}) }); } function showGreeting(message) { $("#greetings").append("" + message + ""); } $(function () { $("form").on('submit', (e) => e.preventDefault()); $( "#connect" ).click(() => connect()); $( "#disconnect" ).click(() => disconnect()); $( "#send" ).click(() => sendName()); }); ```