Tim Carlson
Winter 2024

<info 340/>

Firebase Authentication

View of the Day

  • FirebaseUI (code demo)

  • Managing Auth State (code demo)

  • Firebase Image Uploading (code demo)

Questions?

Firebase Demo 

Firebase Demo 

import React from 'react';
import ReactDOM from 'react-dom/client';
import { initializeApp } from "firebase/app";
import { BrowserRouter } from 'react-router-dom';
import 'bootstrap/dist/css/bootstrap.css';
import './index.css';

import App from './components/App';

// Your web app's Firebase configuration
const firebaseConfig = {
  apiKey: "AIzaSyDb5rO7-hBq0PJ3emVEwv1Uwz8K3Dtz7kk",
  authDomain: "react-chat-wi23a.firebaseapp.com",
  projectId: "react-chat-wi23a",
  storageBucket: "react-chat-wi23a.appspot.com",
  messagingSenderId: "716444400015",
  appId: "1:716444400015:web:a3c6f14042da5bf100f3fa"
};

// Initialize Firebase
const app = initializeApp(firebaseConfig);

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>
);

Index.js

Firebase Demo 

iimport React, { useState, useEffect } from 'react';
import { Routes, Route, Outlet, Navigate, useNavigate } from 'react-router-dom';
import { HeaderBar } from './HeaderBar.js';
import ChatPage from './ChatPage';
import SignInPage from './SignInPage';
import ProfilePage from './ProfilePage.js';
import * as Static from './StaticPages';
import DEFAULT_USERS from '../data/users.json';

export default function App(props) {
  //state
  const [currentUser, setCurrentUser] = useState(DEFAULT_USERS[0]) //default to null user

  const navigateTo = useNavigate(); //navigation hook

  //effect to run when the component first loads
  useEffect(() => {
    //log in a default user
    // loginUser(DEFAULT_USERS[1])

  }, []) //array is list of variables that will cause this to rerun if changed

  const loginUser = (userObj) => {
    console.log("logging in as", userObj.userName);
    setCurrentUser(userObj);
    if(userObj.userId !== null){
      navigateTo('/chat/general'); //go to chat after login
    }
  }

 //... more App on next slide

App part 1

No longer initially logging in a user in App

Reminder this will be passed as a prop to  the Sign-in Component

Firebase Demo 

	//... App continued	
  return (
    <div className="container-fluid d-flex flex-column">
      <HeaderBar currentUser={currentUser} />

      <Routes>
        <Route index element={<Static.WelcomePage />} />
        <Route path="about" element={<Static.AboutPage />} />
        <Route path="*" element={<Static.ErrorPage />} />
        <Route path="signin" element={<SignInPage currentUser={currentUser} loginCallback={loginUser}/>} />

        {/* protected routes */}
        <Route element={<ProtectedPage currentUser={currentUser} />}>
          <Route path="chat/:channelName" element={<ChatPage currentUser={currentUser} />} />
          {/* redirect to general channel */}
          <Route path="chat" element={<Navigate to="/chat/general" />} />
          <Route path="profile" element={<ProfilePage currentUser={currentUser} />}/>
        </Route>
      </Routes>
    </div>
  );
}

function ProtectedPage(props) {
  if(props.currentUser.userId === null) { //if no user, send to sign in
    return <Navigate to="/signin" />
  }
  else { //otherwise, show the child route content
    return <Outlet />
  }
}

App part 2 

import React from 'react';
import { Link, NavLink } from 'react-router-dom';

export function HeaderBar(props) {
  const currentUser = props.currentUser;

  const handleSignOut = (event) => {
    console.log("signing out");
  }  
  
  return (
    <header className="text-light bg-primary px-1 d-flex justify-content-between">
      <h1>React Chat</h1>
      <ul className="nav nav-pills">
        <li className="nav-item">
          <NavLink className="nav-link" to="/">Home</NavLink>
        </li>
        <li className="nav-item">
          <NavLink className="nav-link" to="/chat">Chat</NavLink>
        </li>
        <li className="nav-item">
          <NavLink className="nav-link" to="/about">About</NavLink>
        </li>
        {currentUser.userId && 
          <>
            <li className="nav-item">
              <NavLink to="/profile" className="nav-link">Profile</NavLink>
            </li>
            <li className="nav-item">
              <button className="btn btn-secondary ms-2" onClick={handleSignOut}>Sign Out</button>
            </li>
          </>
        }
        {!currentUser.userId &&
          <li className="nav-item">
            <NavLink className="nav-link" to="/signin">
              <img src={currentUser.userImg} alt={currentUser.userName + " avatar"} />
            </NavLink>
          </li>
        }
      </ul>
    </header>
  )
}

Firebase Demo 

HeaderBar

If we have user, show the Profile Component

If no user, show Sign-in Component

Callback for the onClick on the SignOut Button

Firebase Demo 

export default function ChatPage(props) {
  const [chatMessages, setChatMessages] = useState([]);
...
  useEffect(() => {
    const db = getDatabase(); //"the database"
    const allMessageRef = ref(db, "allMessages");
    const offFunction = onValue(allMessageRef, (snapshot) => {   //when db value changes
    const valueObj = snapshot.val();
    const objKeys = Object.keys(valueObj); //convert object into array
    const objArray = objKeys.map((keyString) => {
    const theMessageObj = valueObj[keyString];
        theMessageObj.key = keyString;
        return theMessageObj;
      })
      setChatMessages(objArray);
    })

    function cleanup() {
      console.log("component is being removed");
    offFunction();     //when the component goes away, we turn off the listener
    }
    return cleanup; //return instructions on how to turn off lights
  }, [])

  const addMessage = (messageText) => {
    const userObj = currentUser;
    const newMessage = {
      "userId": userObj.userId,
      "userName": userObj.userName,
      "userImg": userObj.userImg,
      "text": messageText,
      "timestamp": Date.now(),
      "channel": currentChannel
    }

    const db = getDatabase(); //"the database"
    const allMessageRef = ref(db, 'allMessages');
    firebasePush(allMessageRef, newMessage);
  }
//... More ChatPage below, but won't show on the next slide

ChatPage

The addMessage callback will get the reference to the allMessages key, and then push the new message to the database on Firebase

Use Effect hook to load the messages from firebase when component loads 1st time

Register 'onValue' listener for any change to the Messages on Firebase

If there is a change we get the new messages array and set state to cause re-render

Firebase Demo 

import React, { useState } from 'react';

export default function ProfilePage(props) {
  //convenience
  const displayName = props.currentUser.userName;

  const [imageFile, setImageFile] = useState(undefined)
  let initialURL = props.currentUser.userImg;
  const [imageUrl, setImageUrl] = useState(initialURL)
 
  const handleChange = (event) => {  //image uploading!
    if(event.target.files.length > 0 && event.target.files[0]) { 
      const imageFile = event.target.files[0]
      setImageFile(imageFile);
      setImageUrl(URL.createObjectURL(imageFile));
    }
  }

  const handleImageUpload = (event) => {
    console.log("Uploading", imageFile);
  }

  return (
    <div className="container">
      <h2>
        {props.currentUser.userName && displayName+"'s"} Profile
      </h2>

      <div className="mb-5 image-upload-form">
        <img src={imageUrl} alt="user avatar preview" className="mb-2"/>
        <label htmlFor="imageUploadInput" className="btn btn-sm btn-secondary me-2">Choose Image</label>
        <button className="btn btn-sm btn-success" onClick={handleImageUpload}>Save to Profile</button>
        <input type="file" name="image" id="imageUploadInput" className="d-none" onChange={handleChange}/>
      </div>
    </div>
  )
}

Profile Component 

Firebase Auth

 

Set Up Firebase if you haven't already

  • Create new Firebase Project in Firebase.  

 

Include Firebase in your React app

  • Install (npm install firebase)
  • Import firebase into your app
  • Copy and Paste the config from Firebase Web Console into your code
  • Manage User Profiles
  • Handle Authentication Events (Sign In and Out)

 

 

Firebase Auth with Firebase UI Component

 

Include FirebaseUI in your React app

  • Install (npm install react-firebaseui)
  • Import StyledFirebaseAuth into your app
  • Copy and Paste the uiConfig from FirebaseUI website into your code
  • Manage the signout
  • Handle Authentication Events through effect hook

 

 

FirebaseUI

A library (provided by Firebase) that created a sign-in form for your page.

React 18 workarounds

The React bindings for FirebaseUI (firebaseui-web-react) has not been updated for React 18 yet, and appears to have been abandoned. See the open pull request from Gabriel Villenave.

//in package.json
"react-firebaseui": "https://gitpkg.now.sh/gvillenave/firebaseui-web-react/dist"
# Install library (on command line)
npm install https://gitpkg.now.sh/gvillenave/firebaseui-web-react/dist

A second option is to instead install the firebaseui library (instead of the React bindings) and copy in the StyledFirebaseAuth.tsx file yourself.

Until the pull request is accepted, one work around is to install the updated fork instead of the usual package:

Signin In with FirebaseUI

The FirebaseUI library provides pre-defined UI elements including a "login form" that supports the different authentication methods for the different providers

The <StyledFirebaseAuth> Component can be rendered like any other Component. It requires 2 props:

  • uiConfig - An option containing the configuration values for the login form
  • firebaseAuth - A reference to the "authenticator" service that handles logging in and out. You get access to this through the 'getAuth() function, similar to the 'getDatabase()' function.

We will install the fork instead

import React from 'react';
import { getAuth, EmailAuthProvider, GoogleAuthProvider } from 'firebase/auth';
import { StyledFirebaseAuth } from 'react-firebaseui';
...
export default function SignInPage(props) {
  const currentUser = props.currentUser;
  const loginFunction = props.loginCallback;
  
  const auth = getAuth(); //the authenticator

  const configObj = {
    signInOptions: [
      { 
        provider: EmailAuthProvider.PROVIDER_ID,
        requireDisplayName: true,
      },
      {
        provider: GoogleAuthProvider.PROVIDER_ID
      }
    ],
    signInFlow: 'popup',
    callbacks: {
      signInSuccessWithAuthResult: () => false //don't do anything special on signin
    },
    credentialHelper: 'none'
  }
...
  return (
    <div className="card bg-light">
      <div className="container card-body">
        <StyledFirebaseAuth firebaseAuth={auth} uiConfig={configObj} />
 
        {/* <p className="lead">Pick a user:</p>
        <Dropdown>
...
		</Dropdown> */}
      </div>
    </div>
  )
}

Add firebase config script config and initialize

import

Render component with 2 props

import

get the authenticator

signin

Creating users

Make it easy to test authentication by having consistent simple usernames and passwords

 

  • a@a.com - password: 'password'
  • b@b.com - password: 'password'
  • etc...

Authentication Events (sign in or out)

It is recommended to register an event listener to determine which user is logged in This event occurs when the page first loads and Firebase determines that a user has previously signed up (the “initial state” is set), or when a user logs out. You can register this listener by using the onAuthStateChanged method:

The firebase.auth() variable tracks which User is currently logged in. This info persists even after the browser is closed. Every time page is reloaded, the firebase.auth() function will perform the authentication and “re-login” the user

Firebase Demo 

import React, { useState, useEffect } from 'react';
import { Routes, Route, Outlet, Navigate, useNavigate } from 'react-router-dom';

import { getAuth, onAuthStateChanged } from 'firebase/auth';
...
import DEFAULT_USERS from '../data/users.json';

export default function App(props) {
  //state
  const [currentUser, setCurrentUser] = useState(DEFAULT_USERS[0]) //default to null user

  const navigateTo = useNavigate(); //navigation hook

   //effect to run when the component first loads
   useEffect(() => {
    //log in a default user
    // loginUser(DEFAULT_USERS[1])

    const auth = getAuth();
    //                 authenticator, a callback
    onAuthStateChanged(auth, (firebaseUser) => {
      if(firebaseUser) {
        console.log("signing in as", firebaseUser.displayName)
        console.log(firebaseUser);
        firebaseUser.userId = firebaseUser.uid;
        firebaseUser.userName = firebaseUser.displayName;
        firebaseUser.userImg = firebaseUser.photoURL || "/img/null.png";
        setCurrentUser(firebaseUser);
      }
      else { //no user
        console.log("signed out");
        setCurrentUser(DEFAULT_USERS[0]); //change the null user
      }
    })


  }, []) //array is list of variables that will cause this to rerun if changed
  
  ...

Get App to listen for authchanges and setState

In effect hook (runs 1st time component renders

import getAuth and onAuthStateChanged

getAuth object

Register onAuthStateChanged Listener

If there's a firebaseUser, add some additional fields for user object

Then setState for our currentUser state var

If there is no firebase user, we're signed out

Firebase Demo 

Signout


import { getAuth, signOut } from 'firebase/auth';

const handleSignOut = (event) => {
    //sign out here
    signOut(getAuth());
  }


//...

   <li className="nav-item">
            <button className="btn btn-secondary ms-2" onClick={handleSignOut}>Sign Out</button>
          </li>
...

  //little hacky
  if(currentUser.userId) { //if signed in
    return<Navigate to="/chat/general"/>
  }

  return (
    <div className="card bg-light">
      <div className="container card-body">
	      <StyledFirebaseAuth  uiConfig={configObj} firebaseAuth={auth} />
      </div>
    </div>
  )
}

Signout - in Header.js


import { getAuth, signOut } from 'firebase/auth';

const handleSignOut = (event) => {
    //sign out here
    signOut(getAuth());
  }
//...
   <li className="nav-item">
            <button className="btn btn-secondary ms-2" onClick={handleSignOut}>Sign Out</button>
          </li>

Start App with user Signed - in SignInPage.js

ProfilePage.js - initially before image upload

import React, { useState } from 'react';

export default function ProfilePage(props) {
  const displayName = props.currentUser.userName;
  const [imageFile, setImageFile] = useState(undefined)
  let initialURL = props.currentUser.userImg;
  
  const [imageUrl, setImageUrl] = useState(initialURL)

  const handleChange = (event) => {	  //image uploading!
    if(event.target.files.length > 0 && event.target.files[0]) {
      const imageFile = event.target.files[0]
      setImageFile(imageFile);
      setImageUrl(URL.createObjectURL(imageFile));
    }
  }
  
  const handleImageUpload = (event) => {
    console.log("Uploading", imageFile);
  }

  return (
    <div className="container">
      <h2>
        {props.currentUser.userName && displayName+"'s"} Profile
      </h2>
      <div className="mb-5 image-upload-form">
        <img src={imageUrl} alt="user avatar preview" className="mb-2"/>
        <label htmlFor="imageUploadInput" className="btn btn-sm btn-secondary me-2">Choose Image</label>
        <button className="btn btn-sm btn-success" onClick={handleImageUpload}>Save to Profile</button>
        <input type="file" name="image" id="imageUploadInput" className="d-none" onChange={handleChange}/>
      </div>
    </div>
  )
}

Two state variables

on the file change below, we grab the file and setState on the file, and then create a url, and setState from the file as well

This is just a javascript function to create a url from a file

note display d-none

the label (button) looks better than the file input

here we can handle upload

ProfilePage.js - make file upload look better

import React, { useState } from 'react';

export default function ProfilePage(props) {
  const displayName = props.currentUser.userName;
  const [imageFile, setImageFile] = useState(undefined)
  let initialURL = props.currentUser.userImg;
  
  const [imageUrl, setImageUrl] = useState(initialURL)

  const handleChange = (event) => {	  //image uploading!
    if(event.target.files.length > 0 && event.target.files[0]) {
      const imageFile = event.target.files[0]
      setImageFile(imageFile);
      setImageUrl(URL.createObjectURL(imageFile));
    }
  }
  
  const handleImageUpload = (event) => {
    console.log("Uploading", imageFile);
  }

  return (
    <div className="container">
      <h2>
        {props.currentUser.userName && displayName+"'s"} Profile
      </h2>
      <div className="mb-5 image-upload-form">
        <img src={imageUrl} alt="user avatar preview" className="mb-2"/>
        <label htmlFor="imageUploadInput" className="btn btn-sm btn-secondary me-2">Choose Image</label>
        <button className="btn btn-sm btn-success" onClick={handleImageUpload}>Save to Profile</button>
        <input type="file" name="image" id="imageUploadInput" className="d-none" onChange={handleChange}/>
      </div>
    </div>
  )
}

label htmlFor is a lable for the file input

file input is hidden

The label styled as button, and since its set with htmlFor to the file input it behaves as if clicking on the "choose file"

Saving profile images: Profile.js part 1

import React, { useState } from 'react';

import { getStorage, ref, uploadBytes, getDownloadURL} from 'firebase/storage';
import { updateProfile} from 'firebase/auth';
import { getDatabase, ref as dbRef, set as firebaseSet } from 'firebase/database';

export default function ProfilePage(props) {
  
  const currentUser = props.currentUser;
  const displayName = props.currentUser.userName;

  const [imageFile, setImageFile] = useState(undefined)
  let initialURL = props.currentUser.userImg;
  const [imageUrl, setImageUrl] = useState(initialURL)

  const handleChange = (event) => {
    if(event.target.files.length > 0 && event.target.files[0]) {
      const imageFile = event.target.files[0]
      setImageFile(imageFile);
      setImageUrl(URL.createObjectURL(imageFile));
    }
  }

...//more on next slide

imports to upload storage, update profile, get access to db

Saving profile images: Profile.js part 2

...//profile bottom half of file
  const handleImageUpload = async (event) => {
    console.log("Uploading", imageFile);
    const storage = getStorage();
    const imageRef = ref(storage, "userImages/"+currentUser.userId+".png");
    await uploadBytes(imageRef, imageFile)
    const downloadUrlString = await getDownloadURL(imageRef);
    console.log(downloadUrlString);

    await updateProfile(currentUser, { photoURL: downloadUrlString} ); //put in user profile
 
    const db = getDatabase(); //also put in database (for fun)
    const refString = "userData/"+currentUser.userId+"/imgUrl";
    console.log(refString);
    const userImgRef = dbRef(db, "userData/"+currentUser.userId+"/imgUrl")
    firebaseSet(userImgRef, downloadUrlString);
  }
  return (
    <div className="container">
      <h2>
        {props.currentUser.userName && displayName+"'s"} Profile
      </h2>
      <div className="mb-5 image-upload-form">
        <img src={imageUrl} alt="user avatar preview" className="mb-2"/>
        <label htmlFor="imageUploadInput" className="btn btn-sm btn-secondary me-2">Choose Image</label>
        <button className="btn btn-sm btn-success" onClick={handleImageUpload}>Save to Profile</button>
        <input type="file" name="image" id="imageUploadInput" className="d-none" onChange={handleChange}/>
      </div>
    </div>
  )
}

asynchronous 

getStorage db 

get ref to storage location 

upload bytes there

get the url to it

put it in the userProfile db

store it in the real time db too so we can show it elsewhere easily

Firebase Storage

Allows us to store images, videos, other files

The real time database pretty much stores Strings. You can't store images there.

 

Action Items!

  • Review Everything

  • Late/missing Problem Sets due on Friday

  • Final Project due on Monday 

    • Unless you implement Firebase Auth then it's Tuesday

      Hard deadline! 


info340wi24-firebase-auth

By Tim Carlson

info340wi24-firebase-auth

  • 55