In this article, you will learn how to implement OAuth for Google and GitHub in a Rust frontend application using the Yew.rs framework. Additionally, I will provide backend APIs built using Rust, Node.js, and Golang that you can use alongside the frontend application.

While the Rust backend has support for both Google and GitHub OAuth, only Google OAuth is currently functioning correctly. Therefore, if you intend to use GitHub OAuth, I recommend running the frontend app with either the Node.js or Golang backend APIs.

As this article may be lengthy, feel free to skip to the section that demonstrates how to implement Google and GitHub OAuth. However, if you wish to add other credential options where the user can sign up and sign in with email and password, then you can read the remaining content, which shows how it can be done.

To keep this article concise, I will focus on explaining only the most relevant concepts. However, if you need further explanation on any topic covered in this tutorial, please don’t hesitate to ask. Simply leave a comment below and I’ll do my best to provide you with the information you need. With that said, let’s dive right into the tutorial.

More practice:

Implement Google and GitHub OAuth2 in Rust Frontend App

Run the Google and GitHub OAuth Frontend App

  1. Make sure that you have both Trunk and the WebAssembly target installed. You can install them by following the instructions provided on the Yew.rs documentation’s “Getting Started” page.
  2. Download or clone the Yew.rs project from https://github.com/wpcodevo/rust-yew-google-github-oauth2 and open it in your preferred code editor.
  3. Duplicate the example.env file and rename the copied one to .env. Then, add your Google and GitHub OAuth client IDs to the .env file. If you don’t have these IDs yet or are unsure of how to obtain them, don’t worry! The last two sections of this article provide a step-by-step guide that will walk you through the process.
  4. Open your code editor’s integrated terminal and run the command trunk build. This command installs the necessary crates and creates asset-build pipelines for the assets specified in the index.html file.
  5. Once the installation and asset build process is complete, start the Yew web server by running trunk serve --port 3000 in the terminal. This command creates a local web server on your machine, which you can access via your web browser at localhost:3000.
  6. To interact with the app’s features, such as signing in with Google and GitHub OAuth, registering and signing in with email and password, logging out from the app, and requesting your account details, you need to set up a backend server that the Yew app can make requests to.

    Follow the steps outlined in the subsequent sections to set up backend servers with Rust, Node.js, and Golang.

Run a Rust Backend Server with the App

The Rust backend project has built-in support for both Google and GitHub OAuth, but at the moment, only Google OAuth is fully functional. There is an issue with the GitHub implementation that I am still trying to resolve. The problem appears to be in the get_github_user function located in the /src/github_oauth.rs file. Despite the code being correct, obtaining user information using the access token from GitHub returns a 403 error.

If you want to test both Google and GitHub OAuth, I suggest setting up a Node.js or Golang server that supports both by following the instructions in the “Other Backend APIs with OAuth Support” section below. Also, please leave a comment if you discover a solution to the GitHub OAuth issue in the Rust project, so I can update the project accordingly.

In the meantime, if you’re looking for a comprehensive guide on implementing Google OAuth in Rust, I suggest checking out the post titled “How to Implement Google OAuth2 in Rust“. Alternatively, you can quickly set up the API and interact with it using the Yew.rs application by following the instructions below, without having to write any Rust backend code.

  1. Download or clone the Rust Google and GitHub OAuth2 project from https://github.com/wpcodevo/google-github-oauth2-rust and open the source code in your preferred code editor.
  2. Edit the .env file to include your Google and GitHub OAuth client IDs and secrets. If you’re unsure how to obtain these credentials, you can follow the instructions in the last two sections of this article to get them.
  3. After adding the OAuth credentials to the .env file, install the packages and dependencies required by the project by running the command cargo build.
  4. Start the Actix-Web HTTP server by running the command cargo run.
  5. With the Rust backend server ready to accept requests, you can use the Yew.rs app to interact with it.

Other Backend APIs with OAuth Support

Although I originally intended to include instructions for running the Node.js or Golang server in this article, I ultimately decided against it due to length concerns. However, I have written separate articles that cover these topics, which you can find through the links below.

It’s not necessary to read the entire article, only follow the section that provides instructions on running the project without writing a single line of code.

How the GitHub and Google OAuth Works

Before we dive into the implementation of Google and GitHub OAuth in the Rust frontend project, let me give you an overview of how the OAuth flow works.

Sign In using Google OAuth

Assuming you struggle with remembering passwords, you can take advantage of the Google OAuth option to sign in to the application without using a password. To use Google OAuth, simply click on the “CONTINUE WITH GOOGLE” button.

The Login Page of the Yew.rs Frontend App

After that, the Yew.rs app will generate a Google OAuth consent screen URL and redirect you to it. On the consent screen, you will be prompted to select an account, which will grant the Yew application access to your public Google information such as your email, name, profile picture, and more.

vuejs google oauth authentication consent screen

The Google authorization API will then authenticate the request and redirect it to the callback URL you provided during the application registration process. The callback URL is an endpoint on the backend server that makes additional requests to the Google OAuth endpoints and returns the user’s profile information to the frontend app.

Once the frontend app receives the information, it will redirect you to the profile page where you can view your profile details.

Sign In using GitHub OAuth

In a similar manner, if you prefer to sign into the application using the GitHub OAuth option, you can simply click on the “CONTINUE WITH GITHUB” button. The Yew.rs application will then generate a GitHub OAuth consent screen URL and redirect you to it.

On the consent screen, you will be prompted to authorize access to the Yew application. Once you grant access, GitHub will redirect the request to the redirect URL you provided when you registered the application on GitHub. From there, the backend server will take over, making additional requests to GitHub and returning the GitHub user’s information to the frontend app.

GitHub OAuth Authorization Screen for the NextAuth Project

After the Yew app receives the user’s credentials, it will redirect you to the profile page where your credentials will be displayed.

Access the Account Page After the OAuth Authentication is Successfull

Setup the Rust Frontend Project

Once you finish this project, your file and folder organization should resemble the example shown in the screenshot below. This can serve as a helpful reference as you follow along with the instructions outlined in this article.

File and Folder Structure of the Rust Frontend Google and GitHub OAuth Project

To get started, navigate to the directory where you’d like to create your Rust project and open a new terminal in that location. Then, execute the following commands:


mkdir rust-yew-google-github-oauth2
cd rust-yew-google-github-oauth2
cargo init

This will create a new folder named rust-yew-google-github-oauth2, which will serve as your project’s root directory. The cargo init command will then initialize the folder as a Rust project, creating a new Cargo.toml file and a ‘src‘ directory. Finally, you can install the required crates for this project by running the following commands:


cargo add yew --features csr
cargo add serde_json
cargo add serde --features derive
cargo add chrono --features serde
cargo add reqwasm
cargo add yew-router
cargo add gloo
cargo add validator --features derive
cargo add yewdux
cargo add wasm-bindgen
cargo add web-sys --features "HtmlInputElement Window"
cargo add wasm-bindgen-futures
cargo add url
cargo add load-dotenv

If the latest version of any of the crates causes issues with your application, you can revert to the version specified in the Cargo.toml file below.

Cargo.toml


[package]
name = "rust-yew-google-github-oauth2"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
chrono = { version = "0.4.24", features = ["serde"] }
gloo = "0.8.0"
load-dotenv = "0.1.2"
reqwasm = "0.5.0"
serde = { version = "1.0.160", features = ["derive"] }
serde_json = "1.0.96"
url = "2.3.1"
validator = { version = "0.16.0", features = ["derive"] }
wasm-bindgen = "0.2.84"
wasm-bindgen-futures = "0.4.34"
web-sys = { version = "0.3.61", features = ["HtmlInputElement", "Window"] }
yew = { version = "0.20.0", features = ["csr"] }
yew-router = "0.17.0"
yewdux = "0.9.2"

Generate the GitHub and Google OAuth Consent Screen URLs

To generate the Google and GitHub OAuth consent screen URLs, we’ll start by creating utility functions. Before we can do that, we need to add the OAuth client IDs to the .env file.

.env


SERVER_ENDPOINT=http://localhost:8000

GOOGLE_OAUTH_CLIENT_ID=
GOOGLE_OAUTH_REDIRECT_URL=http://localhost:8000/api/sessions/oauth/google

GITHUB_OAUTH_CLIENT_ID=
GITHUB_OAUTH_REDIRECT_URL=http://localhost:8000/api/sessions/oauth/github

To use the OAuth client IDs in our utility functions, we need to load them from the environment variables. You might think we could use the dotenv crate, which is a popular choice for loading environment variables. However, this won’t work for our project since the frontend application will be running in the browser, and dotenv depends on runtime evaluation.

Instead, we’ll use another crate called load-dotenv that loads environment variables at compile time, and enables us to read them using the std::env!() macro.

Function to Generate the Google OAuth Consent Screen

Let’s create a function that generates the URL for the Google OAuth consent screen. This function will be named get_google_url and will accept a path as an argument, which is the URL where the user should be redirected after authentication is complete.

Inside the function, we’ll use a HashMap to store the necessary parameters for the Google OAuth consent screen. Then, we’ll construct the URL and return it from the function.

To create this function, navigate to the ‘src’ directory and create a new folder called ‘utils’. Inside the ‘utils’ folder, create a file named get_google_url.rs and add the code provided below.

src/utils/get_google_url.rs


use std::collections::HashMap;
use url::Url;

use load_dotenv::load_dotenv;

load_dotenv!();

pub fn get_google_url(from: Option<&str>) -> String {
    let client_id = std::env!("GOOGLE_OAUTH_CLIENT_ID");
    let redirect_uri = std::env!("GOOGLE_OAUTH_REDIRECT_URL");

    let root_url = "https://accounts.google.com/o/oauth2/v2/auth";
    let mut options = HashMap::new();
    options.insert("redirect_uri", redirect_uri);
    options.insert("client_id", client_id);
    options.insert("access_type", "offline");
    options.insert("response_type", "code");
    options.insert("prompt", "consent");
    options.insert(
        "scope",
        "https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email",
    );
    options.insert("state", from.unwrap_or_default());

    let url = Url::parse_with_params(root_url, &options).unwrap();
    let qs = url.query().unwrap();

    format!("{}?{}", root_url, qs)
}

With the get_google_url function defined, you can now use it in the href={} attribute of the Google OAuth button to generate the appropriate URL. When the user clicks on the button, they will be redirected to the Google OAuth consent screen, where they will be asked to grant permission for the application to access their Google account.

Here’s an example code snippet that shows how to use the get_google_url function to create a Google OAuth button with the appropriate link:


<a
  class=""
  style={ "background-color: #3b5998" }
  href={get_google_url(Some("/profile"))}
  role="button"
>
  <img
    class="pr-2"
    src="assets/google.svg"
    alt=""
      style={"height: 2.2rem"}
  />
  {" Continue with Google"}
</a>

Function to Generate the GitHub OAuth Consent Screen

Next, we’ll create a utility function get_github_url that generates the URL for the GitHub OAuth consent screen. This function takes an optional path argument that specifies where to redirect the user after authentication.

To create the function, we’ll follow a similar logic as get_google_url. First, we’ll load the client ID and redirect URL from environment variables using std::env!(). Then, we’ll create a HashMap to hold the parameters required by the GitHub OAuth consent screen. Finally, we’ll construct the URL using the Url crate and return it from the function.

To implement get_github_url, create a new file get_github_url.rs in the src/utils directory, and add the following code:

src/utils/get_github_url.rs


use std::collections::HashMap;
use url::Url;

use load_dotenv::load_dotenv;

load_dotenv!();

pub fn get_github_url(from: Option<&str>) -> String {
    let client_id = std::env!("GITHUB_OAUTH_CLIENT_ID");
    let redirect_uri = std::env!("GITHUB_OAUTH_REDIRECT_URL");

    let root_url = "https://github.com/login/oauth/authorize";
    let mut options = HashMap::new();
    options.insert("redirect_uri", redirect_uri);
    options.insert("client_id", client_id);
    options.insert("scope", "user:email");
    options.insert("state", from.unwrap_or_default());

    let url = Url::parse_with_params(root_url, &options).unwrap();
    let qs = url.query().unwrap();

    format!("{}?{}", root_url, qs)
}

Having defined the get_github_url function, you can now use it to generate the appropriate URL for the GitHub OAuth consent screen in the href={} attribute of your GitHub OAuth button. When the user clicks on the button, they will be redirected to the GitHub OAuth consent screen, where they will be asked to grant permission for the application to access their GitHub account.

Here’s an example code snippet that shows how to use the get_github_url function to create a GitHub OAuth button with the appropriate link:


<a
  class=""
  style={"background-color:#55acee"}
  href={get_github_url(Some("/profile"))}
  role="button"
>
  <img
    class="pr-2"
    src="assets/github.svg"
    alt=""
    style={"height: 2.2rem"}
  />
  {"Continue with GitHub"}
</a>

To allow the utility functions to be used throughout your code, you’ll need to create a mod.rs file in the src/utils directory and export the utility function files as modules. Here’s an example code snippet that shows how to do this:

src/utils/mod.rs


pub mod get_github_url;
pub mod get_google_url;

That’s it! You now have a good understanding of how to implement the frontend logic for Google and GitHub OAuth. But, if you’re interested in taking things further and building pages where users can sign up and sign in using their email and password, then keep reading the article to learn more.

Set up Tailwind CSS for Styling

To make our website look pretty, we’re going to use Tailwind CSS. It’s a popular tool for styling web pages, and even though it’s mostly used with JavaScript, we can still use it in our Rust project. To get started, just run this command:


npx tailwindcss init -p

By running the above command, two new files tailwind.config.js and ‘postcss.config.js’ will be generated in the project’s root directory. We need to update the tailwind.config.js file with specific configurations to help Tailwind CSS recognize the utility classes in our Rust templates. Simply replace the existing configurations in the file with the new ones provided below.

tailwind.config.js


/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./index.html",
    "./src/**/*.{rs,html}"
  ],
  theme: {
    extend: {
      colors: {
        'ct-dark-600': '#222',
        'ct-dark-200': '#e5e7eb',
        'ct-dark-100': '#f5f6f7',
        'ct-blue-600': '#2363eb',
        'ct-yellow-600': '#f9d13e',
      },
      fontFamily: {
        Poppins: ['Poppins, sans-serif'],
      },
      container: {
        center: true,
        padding: '1rem',
        screens: {
          lg: '1125px',
          xl: '1125px',
          '2xl': '1125px',
        },
      },
    },
  },
  plugins: [],
};

To generate the utility classes used in our Rust templates, we need to add Tailwind CSS directives to a CSS file. To achieve this, create a ‘styles‘ folder in the root directory and within this folder, create a tailwind.css file. Then, copy and paste the following code into the newly created file.

styles/tailwind.css


@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap');

@tailwind base;
@tailwind components;
@tailwind utilities;

html {
    font-family: 'Poppins', sans-serif;
}

Now, we need to set up a way to automatically generate the utility classes when any changes are made to the Rust templates. Luckily, we can achieve this using Trunk’s post-build hook feature. To get started, create a Trunk.toml file in the project’s root directory, and add the following configurations to it:

Trunk.toml


[[hooks]]
stage = "post_build"
command = "sh"
command_arguments = ["-c", "npx tailwindcss -i ./styles/tailwind.css -o ./dist/.stage/index.css"]

Finally, to complete the setup, we need to create an index.html file in the project’s root directory that Trunk will use to drive all asset building and bundling. We’ll also include the generated CSS file in the index.html so that our Tailwind CSS can be applied to the Rust templates. To do this, create an index.html file and add the following code to it:

index.html


<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link data-trunk rel="icon" href="logo.svg" type="image/svg" />
    <link rel="stylesheet" href="./index.css" />
    <link data-trunk rel="copy-dir" href="./assets" />
    <title>My First Yew App</title>
  </head>
  <body></body>
</html>

The HTML code above includes a reference to an assets directory where we’ll store the images used in the project. To download this directory, click on this link and unzip the downloaded file. After that, move the assets folder into the project’s root directory.

Create the API Request Functions

To enable communication with the backend server, we need to create API request functions. While these requests can be included in their respective components, it’s better to keep them in a separate module to allow for reuse in multiple components.

We’ll start by defining some structs that represent the structure of the response from the backend server. To do this, navigate to the ‘src‘ directory, create a new folder named api, and add a file named types.rs. The structures provided below should be included in this file.

src/api/types.rs


use chrono::prelude::*;
use serde::{Deserialize, Serialize};

#[allow(non_snake_case)]
#[derive(Debug, Serialize, Deserialize, Default, PartialEq, Clone)]
pub struct User {
    pub id: String,
    pub name: String,
    pub email: String,
    pub role: String,
    pub photo: String,
    pub provider: String,
    pub verified: bool,
    pub createdAt: DateTime<Utc>,
    pub updatedAt: DateTime<Utc>,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct UserData {
    pub user: User,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct UserResponse {
    pub status: String,
    pub data: UserData,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct UserLoginResponse {
    pub status: String,
    pub token: String,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct ErrorResponse {
    pub status: String,
    pub message: String,
}

After defining the necessary response structures in the types.rs file, we can create API request functions that allow us to register, log in, log out, and retrieve user profile information from the backend server. To keep these functions organized and reusable, we’ll create a new module named user_api.rs in the src/api directory.

Inside the user_api.rs file, we’ll define the four API request functions mentioned above. The register and login functions take user credentials as input and return user information upon successful authentication. The logout function does not take any input and simply logs the user out of the current session. Finally, the api_user_info function retrieves information about the currently authenticated user.

src/api/user_api.rs


use super::types::{ErrorResponse, User, UserLoginResponse, UserResponse};
use reqwasm::http;

pub async fn api_register_user(user_data: &str) -> Result<User, String> {
    let response = match http::Request::post("http://localhost:8000/api/auth/register")
        .header("Content-Type", "application/json")
        .body(user_data)
        .send()
        .await
    {
        Ok(res) => res,
        Err(_) => return Err("Failed to make request".to_string()),
    };

    if !response.ok() {
        let error_response = response.json::<ErrorResponse>().await;
        if let Ok(error_response) = error_response {
            return Err(error_response.message);
        } else {
            return Err(format!("API error: {}", response.status()));
        }
    }

    let res_json = response.json::<UserResponse>().await;
    match res_json {
        Ok(data) => Ok(data.data.user),
        Err(_) => Err("Failed to parse response".to_string()),
    }
}

pub async fn api_login_user(credentials: &str) -> Result<UserLoginResponse, String> {
    let response = match http::Request::post("http://localhost:8000/api/auth/login")
        .header("Content-Type", "application/json")
        .credentials(http::RequestCredentials::Include)
        .body(credentials)
        .send()
        .await
    {
        Ok(res) => res,
        Err(_) => return Err("Failed to make request".to_string()),
    };

    if response.status() != 200 {
        let error_response = response.json::<ErrorResponse>().await;
        if let Ok(error_response) = error_response {
            return Err(error_response.message);
        } else {
            return Err(format!("API error: {}", response.status()));
        }
    }

    let res_json = response.json::<UserLoginResponse>().await;
    match res_json {
        Ok(data) => Ok(data),
        Err(_) => Err("Failed to parse response".to_string()),
    }
}

pub async fn api_user_info() -> Result<User, String> {
    let response = match http::Request::get("http://localhost:8000/api/users/me")
        .credentials(http::RequestCredentials::Include)
        .send()
        .await
    {
        Ok(res) => res,
        Err(_) => return Err("Failed to make request".to_string()),
    };

    if response.status() != 200 {
        let error_response = response.json::<ErrorResponse>().await;
        if let Ok(error_response) = error_response {
            return Err(error_response.message);
        } else {
            return Err(format!("API error: {}", response.status()));
        }
    }

    let res_json = response.json::<UserResponse>().await;
    match res_json {
        Ok(data) => Ok(data.data.user),
        Err(_) => Err("Failed to parse response".to_string()),
    }
}

pub async fn api_logout_user() -> Result<(), String> {
    let response = match http::Request::get("http://localhost:8000/api/auth/logout")
        .credentials(http::RequestCredentials::Include)
        .send()
        .await
    {
        Ok(res) => res,
        Err(_) => return Err("Failed to make request".to_string()),
    };

    if response.status() != 200 {
        let error_response = response.json::<ErrorResponse>().await;
        if let Ok(error_response) = error_response {
            return Err(error_response.message);
        } else {
            return Err(format!("API error: {}", response.status()));
        }
    }

    Ok(())
}

With the API request functions and structs defined, the next step is to make them accessible to other parts of the application. To do this, create a new file called mod.rs in the src/api folder and use it to export the types and user_api modules. This will allow other components in the application to use these modules and their associated functions.

src/api/mod.rs


pub mod types;
pub mod user_api;

Manage the Application State Globally

Now, let’s move on to managing the application state. There are various ways to accomplish this task, such as using the Context API provided by Yew. However, for this project, we will use the Yewdux library as it comes with many useful features. We will define the functions that modify the states along with the states themselves in a single file for better organization. So, to get started, create a store.rs file in the src directory and add the code below to it.

src/store.rs


use serde::{Deserialize, Serialize};
use yewdux::prelude::*;

use crate::api::types::User;

#[derive(Debug, PartialEq, Serialize, Deserialize, Default, Clone)]
pub struct AlertInput {
    pub show_alert: bool,
    pub alert_message: String,
}

#[derive(Debug, Serialize, Deserialize, Default, Clone, PartialEq, Store)]
pub struct Store {
    pub auth_user: Option<User>,
    pub page_loading: bool,
    pub alert_input: AlertInput,
}

pub fn set_page_loading(loading: bool, dispatch: Dispatch<Store>) {
    dispatch.reduce_mut(move |store| {
        store.page_loading = loading;
    })
}

pub fn set_auth_user(user: Option<User>, dispatch: Dispatch<Store>) {
    dispatch.reduce_mut(move |store| {
        store.auth_user = user;
    })
}

pub fn set_show_alert(message: String, dispatch: Dispatch<Store>) {
    dispatch.reduce_mut(move |store| {
        store.alert_input = AlertInput {
            alert_message: message,
            show_alert: true,
        };
    })
}

pub fn set_hide_alert(dispatch: Dispatch<Store>) {
    dispatch.reduce_mut(move |store| {
        store.alert_input.show_alert = false;
    })
}

Create Some Reusable Components

As we continue building out our web app, it’s important to keep in mind the principles of DRY (Don’t Repeat Yourself) and modularity. One way to accomplish this is by creating reusable components that can be used in multiple parts of our application.

In this case, we’ll be creating several key components, including a Spinner, Form input, Alert notification, and Header component. By doing so, we can save time and effort by avoiding repetitive code, and maintain a more organized and scalable codebase.

Create a Spinner Component

To implement the Spinner component, we’ll begin by creating a new directory called ‘components‘ inside the ‘src‘ folder. Within the ‘components‘ folder, we’ll then create a spinner.rs file and add the code below. This component will be responsible for displaying a spinner whenever a request is being processed by the backend server.

src/components/spinner.rs


use yew::prelude::*;

#[derive(Debug, Properties, PartialEq)]
pub struct Props {
    #[prop_or("1.25rem".to_string())]
    pub width: String,
    #[prop_or("1.25rem".to_string())]
    pub height: String,
    #[prop_or("text-gray-200".to_string())]
    pub color: String,
    #[prop_or("fill-blue-600".to_string())]
    pub bg_color: String,
}

#[function_component(Spinner)]
pub fn spinner_component(props: &Props) -> Html {
    html! {
    <svg
      role="status"
      class={format!(
        "mr-2 {} animate-spin dark:text-gray-600 {} h-5",
        props.color.clone(), props.bg_color.clone()
      )}
      style={format!("height:{};width:{}", props.width.clone(), props.height.clone())}
      viewBox="0 0 100 101"
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
    >
      <path
        d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
        fill="currentColor"
      />
      <path
        d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
        fill="currentFill"
      />
    </svg>
    }
}

Create a Form Input Component

Let’s move on to creating the Form Input component, which we can reuse in any form that needs an input field. To define the component, create a new file named form_input.rs in the src/components directory and copy the code provided below.

src/components/form_input.rs


use std::{cell::RefCell, rc::Rc};

use validator::ValidationErrors;
use wasm_bindgen::JsCast;
use web_sys::HtmlInputElement;
use yew::prelude::*;

#[derive(Properties, PartialEq)]
pub struct Props {
    #[prop_or("text".to_string())]
    pub input_type: String,
    pub label: String,
    pub name: String,
    pub input_ref: NodeRef,
    pub handle_onchange: Callback<String>,
    pub handle_on_input_blur: Callback<(String, String)>,
    pub errors: Rc<RefCell<ValidationErrors>>,
}

#[function_component(FormInput)]
pub fn form_input_component(props: &Props) -> Html {
    let val_errors = props.errors.borrow();
    let errors = val_errors.field_errors().clone();
    let empty_errors = vec![];
    let error = match errors.get(&props.name.as_str()) {
        Some(error) => error,
        None => &empty_errors,
    };
    let error_message = match error.get(0) {
        Some(message) => message.to_string(),
        None => "".to_string(),
    };

    let handle_onchange = props.handle_onchange.clone();
    let onchange = Callback::from(move |event: Event| {
        let target = event.target().unwrap();
        let value = target.unchecked_into::<HtmlInputElement>().value();
        handle_onchange.emit(value);
    });

    let handle_on_input_blur = props.handle_on_input_blur.clone();
    let on_blur = {
        let cloned_input_name = props.name.clone();
        Callback::from(move |event: FocusEvent| {
            let input_name = cloned_input_name.clone();
            let target = event.target().unwrap();
            let value = target.unchecked_into::<HtmlInputElement>().value();
            handle_on_input_blur.emit((input_name, value));
        })
    };

    html! {
    <div class="mb-6">
      <input
        type={props.input_type.clone()}
        placeholder={props.label.clone()}
        class="form-control block w-full px-4 py-5 text-sm font-normal text-gray-700 bg-white bg-clip-padding border border-solid border-gray-300 rounded transition ease-in-out m-0 focus:text-gray-700 focus:bg-white focus:border-blue-600 focus:outline-none"
        ref={props.input_ref.clone()}
        onchange={onchange}
        onblur={on_blur}
      />
    <span class="text-red-700 text-sm mt-1">
        {error_message}
    </span>
    </div>
    }
}

Create an Alert Notification Component

Let’s now implement an alert notification component that can display success or error messages returned by the backend server. To get started, create a new file named alert.rs within the src/components directory and paste the following code:

src/components/alert.rs


use gloo::timers::callback::Timeout;
use yew::prelude::*;
use yewdux::prelude::use_store;

use crate::store::{set_hide_alert, Store};

#[derive(Debug, PartialEq, Properties)]
pub struct Props {
    pub message: String,
    pub delay_ms: u32,
}

#[function_component(AlertComponent)]
pub fn alert_component(props: &Props) -> Html {
    let (store, dispatch) = use_store::<Store>();
    let show_alert = store.alert_input.show_alert;

    use_effect_with_deps(
        move |(show_alert, dispatch, delay_ms)| {
            let cloned_dispatch = dispatch.clone();
            if *show_alert {
                let handle =
                    Timeout::new(*delay_ms, move || set_hide_alert(cloned_dispatch)).forget();
                let clear_handle = move || {
                    web_sys::Window::clear_timeout_with_handle(
                        &web_sys::window().unwrap(),
                        handle.as_f64().unwrap() as i32,
                    );
                };

                Box::new(clear_handle) as Box<dyn FnOnce()>
            } else {
                Box::new(|| {}) as Box<dyn FnOnce()>
            }
        },
        (show_alert, dispatch.clone(), props.delay_ms),
    );

    html! {
    <div id="myToast" class={format!("fixed top-14 right-10 px-5 py-4 border-r-8 border-orange-500 bg-white drop-shadow-lg {}", if show_alert { "" } else { "hidden" })}>
        <p class="text-sm">
            <span class="mr-2 inline-block px-3 py-1 rounded-full bg-blue-500 text-white font-extrabold">{"i"}</span>
            {props.message.clone()}
        </p>
    </div>
    }
}

Create a Header Component

To enable us to reuse the header across different page components and provide the functionality to sign out the currently logged-in user, we’ll create a Header component. This component will contain a navigation menu with links to different pages, depending on whether the user is logged in or not. To create this component, we’ll add the following code to a new header.rs file located in the src/components directory.

src/components/header.rs


use crate::{
    api::user_api::api_logout_user,
    router::{self, Route},
    store::{set_auth_user, set_page_loading, set_show_alert, Store},
};
use wasm_bindgen_futures::spawn_local;
use yew::prelude::*;
use yew_router::prelude::*;
use yewdux::prelude::*;

#[function_component(Header)]
pub fn header_component() -> Html {
    let (store, dispatch) = use_store::<Store>();
    let user = store.auth_user.clone();
    let navigator = use_navigator().unwrap();

    let handle_logout = {
        let store_dispatch = dispatch.clone();
        let cloned_navigator = navigator.clone();

        Callback::from(move |_: MouseEvent| {
            let dispatch = store_dispatch.clone();
            let navigator = cloned_navigator.clone();
            spawn_local(async move {
                set_page_loading(true, dispatch.clone());
                let res = api_logout_user().await;
                match res {
                    Ok(_) => {
                        set_page_loading(false, dispatch.clone());
                        set_auth_user(None, dispatch.clone());
                        set_show_alert("Logged out successfully".to_string(), dispatch);
                        navigator.push(&router::Route::LoginPage);
                    }
                    Err(e) => {
                        set_show_alert(e.to_string(), dispatch.clone());
                        set_page_loading(false, dispatch);
                    }
                };
            });
        })
    };

    html! {
        <header class="bg-white h-20">
        <nav class="h-full flex justify-between container items-center">
          <div>
            <Link<Route> to={Route::HomePage} classes="text-ct-dark-600">{"CodevoWeb"}</Link<Route>>
          </div>
          <ul class="flex items-center gap-4">
            <li>
              <Link<Route> to={Route::HomePage} classes="text-ct-dark-600">{"Home"}</Link<Route>>
            </li>
            if user.is_some() {
               <>
                <li>
                  <Link<Route> to={Route::ProfilePage} classes="text-ct-dark-600">{"Profile"}</Link<Route>>
                </li>
                <li
                  class="cursor-pointer"
                >
                  {"Create Post"}
                </li>
                <li class="cursor-pointer" onclick={handle_logout}>
                  {"Logout"}
                </li>
              </>

            } else {
              <>
                <li>
                  <Link<Route> to={Route::RegisterPage} classes="text-ct-dark-600">{"SignUp"}</Link<Route>>
                </li>
                <li>
                  <Link<Route> to={Route::LoginPage} classes="text-ct-dark-600">{"Login"}</Link<Route>>
                </li>
              </>
            }
          </ul>
        </nav>
      </header>
    }
}

To make these reusable components accessible to other parts of the application, create a mod.rs file in the src/components directory and add the code below to export them.

src/components/mod.rs


pub mod alert;
pub mod form_input;
pub mod header;
pub mod spinner;

Create the Frontend Pages for the Authentication

We’ve completed most of the groundwork for our application by building the API request functions, designing reusable Yew components, and defining the application states using Yewdux. Now, we can integrate these parts by creating page components that leverage them. The pages we’ll create include the login page, account registration page, home page, and profile page.

Account Registration Page

To begin, let’s focus on creating the signup page component. This component will enable users to input their details and create a new account within our application. To get started, you’ll need to create a ‘pages‘ directory in the ‘src‘ folder. Within this ‘pages‘ directory, you can create a register_page.rs file and add the necessary code to create the component.

src/pages/register_page.rs


use std::cell::RefCell;
use std::ops::Deref;
use std::rc::Rc;

use crate::api::user_api::api_register_user;
use crate::components::form_input::FormInput;
use crate::components::header::Header;
use crate::router;
use crate::store::{set_page_loading, set_show_alert, Store};

use serde::{Deserialize, Serialize};
use validator::{Validate, ValidationErrors};
use wasm_bindgen_futures::spawn_local;
use web_sys::HtmlInputElement;
use yew::prelude::*;
use yew_router::prelude::*;
use yewdux::prelude::*;

#[derive(Validate, Debug, Default, Clone, Serialize, Deserialize)]

struct RegisterUserSchema {
    #[validate(length(min = 1, message = "Name is required"))]
    name: String,
    #[validate(
        length(min = 1, message = "Email is required"),
        email(message = "Email is invalid")
    )]
    email: String,
    #[validate(
        length(min = 1, message = "Password is required"),
        length(min = 6, message = "Password must be at least 6 characters")
    )]
    password: String,
    #[validate(
        length(min = 1, message = "Please confirm your password"),
        must_match(other = "password", message = "Passwords do not match")
    )]
    password_confirm: String,
}

fn get_input_callback(
    name: &'static str,
    cloned_form: UseStateHandle<RegisterUserSchema>,
) -> Callback<String> {
    Callback::from(move |value| {
        let mut data = cloned_form.deref().clone();
        match name {
            "name" => data.name = value,
            "email" => data.email = value,
            "password" => data.password = value,
            "password_confirm" => data.password_confirm = value,
            _ => (),
        }
        cloned_form.set(data);
    })
}

#[function_component(RegisterPage)]
pub fn register_page() -> Html {
    let (_, dispatch) = use_store::<Store>();
    let form = use_state(|| RegisterUserSchema::default());
    let validation_errors = use_state(|| Rc::new(RefCell::new(ValidationErrors::new())));
    let navigator = use_navigator().unwrap();

    let name_input_ref = NodeRef::default();
    let email_input_ref = NodeRef::default();
    let password_input_ref = NodeRef::default();
    let password_confirm_input_ref = NodeRef::default();

    let validate_input_on_blur = {
        let cloned_form = form.clone();
        let cloned_validation_errors = validation_errors.clone();
        Callback::from(move |(name, value): (String, String)| {
            let mut data = cloned_form.deref().clone();
            match name.as_str() {
                "email" => data.email = value,
                "password" => data.password = value,
                _ => (),
            }
            cloned_form.set(data);

            match cloned_form.validate() {
                Ok(_) => {
                    cloned_validation_errors
                        .borrow_mut()
                        .errors_mut()
                        .remove(name.as_str());
                }
                Err(errors) => {
                    cloned_validation_errors
                        .borrow_mut()
                        .errors_mut()
                        .retain(|key, _| key != &name);
                    for (field_name, error) in errors.errors() {
                        if field_name == &name {
                            cloned_validation_errors
                                .borrow_mut()
                                .errors_mut()
                                .insert(field_name.clone(), error.clone());
                        }
                    }
                }
            }
        })
    };

    let handle_name_input = get_input_callback("name", form.clone());
    let handle_email_input = get_input_callback("email", form.clone());
    let handle_password_input = get_input_callback("password", form.clone());
    let handle_password_confirm_input = get_input_callback("password_confirm", form.clone());

    let on_submit = {
        let cloned_form = form.clone();
        let cloned_validation_errors = validation_errors.clone();
        let cloned_navigator = navigator.clone();
        let cloned_dispatch = dispatch.clone();

        let cloned_name_input_ref = name_input_ref.clone();
        let cloned_email_input_ref = email_input_ref.clone();
        let cloned_password_input_ref = password_input_ref.clone();
        let cloned_password_confirm_input_ref = password_confirm_input_ref.clone();

        Callback::from(move |event: SubmitEvent| {
            let form = cloned_form.clone();
            let validation_errors = cloned_validation_errors.clone();
            let navigator = cloned_navigator.clone();
            let dispatch = cloned_dispatch.clone();

            let name_input_ref = cloned_name_input_ref.clone();
            let email_input_ref = cloned_email_input_ref.clone();
            let password_input_ref = cloned_password_input_ref.clone();
            let password_confirm_input_ref = cloned_password_confirm_input_ref.clone();

            event.prevent_default();
            spawn_local(async move {
                match form.validate() {
                    Ok(_) => {
                        let form_data = form.deref().clone();
                        let form_json = serde_json::to_string(&form_data).unwrap();
                        set_page_loading(true, dispatch.clone());

                        let name_input = name_input_ref.cast::<HtmlInputElement>().unwrap();
                        let email_input = email_input_ref.cast::<HtmlInputElement>().unwrap();
                        let password_input = password_input_ref.cast::<HtmlInputElement>().unwrap();
                        let password_confirm_input = password_confirm_input_ref
                            .cast::<HtmlInputElement>()
                            .unwrap();

                        name_input.set_value("");
                        email_input.set_value("");
                        password_input.set_value("");
                        password_confirm_input.set_value("");

                        let res = api_register_user(&form_json).await;
                        match res {
                            Ok(_) => {
                                set_page_loading(false, dispatch.clone());
                                set_show_alert(
                                    "Account registered successfully".to_string(),
                                    dispatch,
                                );
                                navigator.push(&router::Route::LoginPage);
                            }
                            Err(e) => {
                                set_page_loading(false, dispatch.clone());
                                set_show_alert(e.to_string(), dispatch);
                            }
                        };
                    }
                    Err(e) => {
                        validation_errors.set(Rc::new(RefCell::new(e)));
                    }
                }
            });
        })
    };

    html! {
    <>
    <Header />
    <section class="bg-ct-blue-600 min-h-screen pt-20">
      <div class="container mx-auto px-6 py-12 h-full flex justify-center items-center">
        <div class="md:w-8/12 lg:w-5/12 bg-white px-8 py-10">
          <form onsubmit={on_submit}>
            <FormInput label="Full Name" name="name" input_ref={name_input_ref} handle_onchange={handle_name_input}  errors={&*validation_errors} handle_on_input_blur={validate_input_on_blur.clone()} />
            <FormInput label="Email" name="email" input_type="email" input_ref={email_input_ref} handle_onchange={handle_email_input}  errors={&*validation_errors} handle_on_input_blur={validate_input_on_blur.clone()} />
            <FormInput label="Password" name="password" input_type="password" input_ref={password_input_ref} handle_onchange={handle_password_input}  errors={&*validation_errors} handle_on_input_blur={validate_input_on_blur.clone()} />
            <FormInput
              label="Confirm Password"
              name="password_confirm"
              input_type="password"
              input_ref={password_confirm_input_ref}
              handle_onchange={handle_password_confirm_input}
              errors={&*validation_errors}
              handle_on_input_blur={validate_input_on_blur.clone()}
            />
            <button
              type="submit"
              class="inline-block px-7 py-4 bg-blue-600 text-white font-medium text-sm leading-snug uppercase rounded shadow-md hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 active:shadow-lg transition duration-150 ease-in-out w-full"
            >
              {"Sign up"}
            </button>
          </form>
          </div>
      </div>
    </section>
    </>
    }
}

Account Login Page

Let’s move on to creating the login page component, which serves a unique purpose in our application. This component features a form where registered users can enter their email and password to log in. In addition, it also provides the option for users to sign in through their Google or GitHub accounts using OAuth.

To create the login page component, navigate to the src/pages directory and create a new file called login_page.rs. Then, copy and paste the code below into the file.

src/pages/login_page.rs


use std::cell::RefCell;
use std::ops::Deref;
use std::rc::Rc;

use crate::api::user_api::api_login_user;
use crate::components::form_input::FormInput;
use crate::components::header::Header;
use crate::router;
use crate::store::{set_page_loading, set_show_alert, Store};
use crate::utils::get_github_url::get_github_url;
use crate::utils::get_google_url::get_google_url;

use serde::{Deserialize, Serialize};
use validator::{Validate, ValidationErrors};
use wasm_bindgen_futures::spawn_local;
use web_sys::HtmlInputElement;
use yew::prelude::*;
use yew_router::prelude::*;
use yewdux::prelude::*;

#[derive(Validate, Debug, Default, Clone, Serialize, Deserialize)]

struct LoginUserSchema {
    #[validate(
        length(min = 1, message = "Email is required"),
        email(message = "Email is invalid")
    )]
    email: String,
    #[validate(
        length(min = 1, message = "Password is required"),
        length(min = 6, message = "Password must be at least 6 characters")
    )]
    password: String,
}

fn get_input_callback(
    name: &'static str,
    cloned_form: UseStateHandle<LoginUserSchema>,
) -> Callback<String> {
    Callback::from(move |value| {
        let mut data = cloned_form.deref().clone();
        match name {
            "email" => data.email = value,
            "password" => data.password = value,
            _ => (),
        }
        cloned_form.set(data);
    })
}

#[function_component(LoginPage)]
pub fn login_page() -> Html {
    let (_, dispatch) = use_store::<Store>();
    let form = use_state(|| LoginUserSchema::default());
    let validation_errors = use_state(|| Rc::new(RefCell::new(ValidationErrors::new())));
    let navigator = use_navigator().unwrap();

    let email_input_ref = NodeRef::default();
    let password_input_ref = NodeRef::default();

    let validate_input_on_blur = {
        let cloned_form = form.clone();
        let cloned_validation_errors = validation_errors.clone();
        Callback::from(move |(name, value): (String, String)| {
            let mut data = cloned_form.deref().clone();
            match name.as_str() {
                "email" => data.email = value,
                "password" => data.password = value,
                _ => (),
            }
            cloned_form.set(data);

            match cloned_form.validate() {
                Ok(_) => {
                    cloned_validation_errors
                        .borrow_mut()
                        .errors_mut()
                        .remove(name.as_str());
                }
                Err(errors) => {
                    cloned_validation_errors
                        .borrow_mut()
                        .errors_mut()
                        .retain(|key, _| key != &name);
                    for (field_name, error) in errors.errors() {
                        if field_name == &name {
                            cloned_validation_errors
                                .borrow_mut()
                                .errors_mut()
                                .insert(field_name.clone(), error.clone());
                        }
                    }
                }
            }
        })
    };

    let handle_email_input = get_input_callback("email", form.clone());
    let handle_password_input = get_input_callback("password", form.clone());

    let on_submit = {
        let cloned_form = form.clone();
        let cloned_validation_errors = validation_errors.clone();
        let store_dispatch = dispatch.clone();
        let cloned_navigator = navigator.clone();

        let cloned_email_input_ref = email_input_ref.clone();
        let cloned_password_input_ref = password_input_ref.clone();

        Callback::from(move |event: SubmitEvent| {
            event.prevent_default();

            let dispatch = store_dispatch.clone();
            let form = cloned_form.clone();
            let validation_errors = cloned_validation_errors.clone();
            let navigator = cloned_navigator.clone();

            let email_input_ref = cloned_email_input_ref.clone();
            let password_input_ref = cloned_password_input_ref.clone();

            spawn_local(async move {
                match form.validate() {
                    Ok(_) => {
                        let form_data = form.deref().clone();
                        set_page_loading(true, dispatch.clone());

                        let email_input = email_input_ref.cast::<HtmlInputElement>().unwrap();
                        let password_input = password_input_ref.cast::<HtmlInputElement>().unwrap();

                        email_input.set_value("");
                        password_input.set_value("");

                        let form_json = serde_json::to_string(&form_data).unwrap();
                        let res = api_login_user(&form_json).await;
                        match res {
                            Ok(_) => {
                                set_page_loading(false, dispatch);
                                navigator.push(&router::Route::ProfilePage);
                            }
                            Err(e) => {
                                set_page_loading(false, dispatch.clone());
                                set_show_alert(e.to_string(), dispatch);
                            }
                        };
                    }
                    Err(e) => {
                        validation_errors.set(Rc::new(RefCell::new(e)));
                    }
                }
            });
        })
    };

    html! {
    <>
      <Header />
      <section class="bg-ct-blue-600 min-h-screen pt-20">
      <div class="container mx-auto px-6 py-12 h-full flex justify-center items-center">
        <div class="md:w-8/12 lg:w-5/12 bg-white px-8 py-10">
          <form
            onsubmit={on_submit}
          >
            <FormInput label="Email" name="email" input_type="email" input_ref={email_input_ref} handle_onchange={handle_email_input} errors={&*validation_errors} handle_on_input_blur={validate_input_on_blur.clone()} />
            <FormInput label="Password" name="password" input_type="password" input_ref={password_input_ref} handle_onchange={handle_password_input} errors={&*validation_errors} handle_on_input_blur={validate_input_on_blur.clone()}/>

            <div class="flex justify-between items-center mb-6">
              <div class="form-group form-check">
                <input
                  type="checkbox"
                  class="form-check-input appearance-none h-4 w-4 border border-gray-300 rounded-sm bg-white checked:bg-blue-600 checked:border-blue-600 focus:outline-none transition duration-200 mt-1 align-top bg-no-repeat bg-center bg-contain float-left mr-2 cursor-pointer"
                  id="exampleCheck3"
                />
                <label
                  class="form-check-label inline-block text-gray-800"
                  html="exampleCheck2"
                >
                  {"Remember me"}
                </label>
              </div>
              <a
                href="#!"
                class="text-blue-600 hover:text-blue-700 focus:text-blue-700 active:text-blue-800 duration-200 transition ease-in-out"
              >
                {"Forgot password?"}
              </a>
            </div>

            <button
              type="submit"
              class="inline-block px-7 py-4 bg-blue-600 text-white font-medium text-sm leading-snug uppercase rounded shadow-md hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 active:shadow-lg transition duration-150 ease-in-out w-full"
            >
              {"Sign in"}
            </button>

            <div class="flex items-center my-4 before:flex-1 before:border-t before:border-gray-300 before:mt-0.5 after:flex-1 after:border-t after:border-gray-300 after:mt-0.5">
              <p class="text-center font-semibold mx-4 mb-0">{"OR"}</p>
            </div>

            <a
              class="px-7 py-2 text-white font-medium text-sm leading-snug uppercase rounded shadow-md hover:shadow-lg focus:shadow-lg focus:outline-none focus:ring-0 active:shadow-lg transition duration-150 ease-in-out w-full flex justify-center items-center mb-3"
              style={ "background-color: #3b5998" }
              href={get_google_url(Some("/profile"))}
              role="button"
            >
              <img
                class="pr-2"
                src="assets/google.svg"
                alt=""
                 style={"height: 2.2rem"}
              />
             {" Continue with Google"}
            </a>
            <a
              class="px-7 py-2 text-white font-medium text-sm leading-snug uppercase rounded shadow-md hover:shadow-lg focus:shadow-lg focus:outline-none focus:ring-0 active:shadow-lg transition duration-150 ease-in-out w-full flex justify-center items-center"
              style={"background-color:#55acee"}
              href={get_github_url(Some("/profile"))}
              role="button"
            >
              <img
                class="pr-2"
                src="assets/github.svg"
                alt=""
                style={"height: 2.2rem"}
              />
              {"Continue with GitHub"}
            </a>
          </form>
          </div>
      </div>
    </section>
    </>
    }
}

Profile Page

Next, we’ll create the profile page component, which can only be accessed by logged-in users with valid cookies from the backend server. This page will display the account information of the currently logged-in user. To create this component, simply create a new file called profile_page.rs in the src/pages directory, and add the provided code.

src/pages/profile_page.rs


use crate::{
    api::user_api::api_user_info,
    components::header::Header,
    router,
    store::{set_auth_user, set_page_loading, set_show_alert, Store},
};
use yew::prelude::*;
use yew_router::prelude::use_navigator;
use yewdux::prelude::*;

#[function_component(ProfilePage)]
pub fn profile_page() -> Html {
    let (store, dispatch) = use_store::<Store>();
    let user = store.auth_user.clone();
    let navigator = use_navigator().unwrap();

    use_effect_with_deps(
        move |_| {
            let dispatch = dispatch.clone();
            wasm_bindgen_futures::spawn_local(async move {
                set_page_loading(true, dispatch.clone());
                let response = api_user_info().await;
                match response {
                    Ok(user) => {
                        set_page_loading(false, dispatch.clone());
                        set_auth_user(Some(user), dispatch);
                    }
                    Err(e) => {
                        set_page_loading(false, dispatch.clone());
                        set_show_alert(e.to_string(), dispatch);
                        navigator.push(&router::Route::LoginPage);
                    }
                }
            });
        },
        (),
    );

    html! {
    <>
      <Header />
      <section class="bg-ct-blue-600 min-h-screen pt-20">
        <div class="max-w-4xl mx-auto bg-ct-dark-100 rounded-md h-[20rem] flex justify-center items-center">
          <div>
            <p class="text-5xl font-semibold">{"Profile Page"}</p>
            if let Some(user) = user {
              <div class="flex items-center gap-8">
                <div>
                  <img
                      src={
                          if user.photo.contains("default.png") {
                              format!("http://localhost:8000/api/images/{}", user.photo)
                          } else {
                              user.photo
                          }
                      }
                      class="max-h-36"
                      alt={format!("profile photo of {}", user.name)}
                  />

                </div>
                <div class="mt-8">
                  <p class="mb-4">{format!("ID: {}", user.id)}</p>
                  <p class="mb-4">{format!("Name: {}", user.name)}</p>
                  <p class="mb-4">{format!("Email: {}", user.email)}</p>
                  <p class="mb-4">{format!("Role: {}", user.role)}</p>
                  <p class="mb-4">{format!("Provider: {}", user.provider)}</p>
                </div>
              </div>
            }else {
              <p class="mb-4">{"Loading..."}</p>
            }
          </div>
        </div>
      </section>
    </>
    }
}

Home Page

Finally, we’ll create the home page component. This component is straightforward and will display a simple message to the user. To get started, create a file named home_page.rs in the src/pages directory and add the following code to it.

src/pages/home_page.rs


use crate::components::header::Header;
use yew::prelude::*;

#[function_component(HomePage)]
pub fn home_page() -> Html {
    html! {
      <>
        <Header />
        <section class="bg-ct-blue-600 min-h-screen pt-20">
            <div class="max-w-4xl mx-auto bg-ct-dark-100 rounded-md h-[20rem] flex justify-center items-center">
                <p class="text-3xl font-semibold">{"Welcome to Google OAuth2 with Yew.rs"}</p>
            </div>
        </section>
      </>
    }
}

To enable us to define routes for the pages we just created, we need to export their respective modules. To do this, create a file named mod.rs and include the following code, which will allow us to use the page components in another module:

src/components/mod.rs


pub mod home_page;
pub mod login_page;
pub mod profile_page;
pub mod register_page;

Create Routes for the Page Components

Now that we’ve built all the page components, we’re ready to create routes for them. To do this, we’ll create a router.rs file in the ‘src‘ directory and add the following code. This code imports the necessary modules and defines the routes for our application. Each route is associated with a specific page component, which is rendered using the ‘switch‘ function when the user navigates to the corresponding URL.

src/router.rs


use yew::prelude::*;
use yew_router::prelude::*;

use crate::pages::{
    home_page::HomePage, login_page::LoginPage, profile_page::ProfilePage,
    register_page::RegisterPage,
};

#[derive(Clone, Routable, PartialEq)]
pub enum Route {
    #[at("/")]
    HomePage,
    #[at("/register")]
    RegisterPage,
    #[at("/login")]
    LoginPage,
    #[at("/profile")]
    ProfilePage,
}

pub fn switch(routes: Route) -> Html {
    match routes {
        Route::HomePage => html! {<HomePage/> },
        Route::RegisterPage => html! {<RegisterPage/> },
        Route::LoginPage => html! {<LoginPage/> },
        Route::ProfilePage => html! {<ProfilePage/> },
    }
}

Create the App Component

In order to register the routes we defined earlier, we will use the BrowserRouter component provided by the Yew Router library. We’ll also render the AlertComponent and Spinner components in this file. Although we could put this code in the main file, it’s generally good practice to create an App component that handles these tasks to keep the main file clean and organized.

To get started, create a new file named app.rs in the src directory and add the following code:

src/app.rs


use crate::components::{
    alert::{AlertComponent, Props as AlertProps},
    spinner::Spinner,
};
use yew::prelude::*;
use yew_router::prelude::*;
use yewdux::prelude::use_store;

use crate::router::{switch, Route};
use crate::store::Store;

#[function_component(App)]
pub fn app() -> Html {
    let (store, _) = use_store::<Store>();
    let message = store.alert_input.alert_message.clone();
    let show_alert = store.alert_input.show_alert;
    let is_page_loading = store.page_loading.clone();

    let alert_props = AlertProps {
        message,
        delay_ms: 5000,
    };
    html! {
        <BrowserRouter>
                <Switch<Route> render={switch} />
                if show_alert {
                    <AlertComponent
                        message={alert_props.message}
                        delay_ms={alert_props.delay_ms}
                     />
                }
                if is_page_loading {
                    <div class="pt-4 pl-2 top-[5.5rem] fixed">
                        <Spinner width="1.5rem" height="1.5rem" color="text-ct-yellow-600" />
                    </div>
                }
        </BrowserRouter>
    }
}

Render the App Component in the Main File

We’re almost done! We just need to import all the modules we’ve created so far and render the App component we defined earlier. Open the src/main.rs file and replace its content with the following code:

src/main.rs


mod api;
mod app;
mod components;
mod pages;
mod router;
mod store;
mod utils;

fn main() {
    yew::Renderer::<app::App>::new().render();
}

Congratulations! You’ve now completed the project by building a full-featured frontend application in Rust using the Yew.rs framework. With this application, users can easily sign up for an account, log in and out, and access protected routes. Additionally, users can sign in with their Google or GitHub accounts using OAuth.

To get started with the application, simply ensure that the backend server is up and running. Once it is, you can then begin to interact with the frontend app to create an account, log in, and explore all of its exciting features.

How to Obtain the Google OAuth Credentials

In this section, you’ll learn how to obtain the Google OAuth2 client ID and secret from the Google Cloud Console. Here are the steps to follow:

  1. Go to https://console.developers.google.com/ and make sure you’re signed in to your Google account.
  2. Click the dropdown menu at the top of the page to display a pop-up window. From there, you can choose an existing project or create a new one.select a project or create a new one on the Google Cloud API dashboard
  3. To create a new project, click the “New Project” button in the top-right corner of the pop-up window. Enter a name for your project and click the “Create” button to complete the process.
    create a new project on the google console api dashboard
  4. Once your project is created, click the “SELECT PROJECT” button from the notifications.
    click on the newly created project from the notification
  5. Click the “OAuth consent screen” menu on the left sidebar. Choose “External” as the “User Type” and click on the “CREATE” button.
    select external under the user type and click on create
  6. On the “Edit app registration” screen, go to the “App information” section and fill in the required details, including a logo for the consent screen.
    provide the consent screen credentials part 1
    Under the “App domain” section, provide links to your homepage, privacy policy, and terms of service pages. Input your email address under the “Developer contact information” section and click on the “SAVE AND CONTINUE” button.
    provide the consent screen credentials part 2
  7. On the “Scopes” screen, click on the “ADD OR REMOVE SCOPES” button, select .../auth/userinfo.email and .../auth/userinfo.profile from the list of options, and then click on the “UPDATE” button. Scroll down and click the “SAVE AND CONTINUE” button.
    select the scopes
  8. On the “Test users” screen, add the email addresses of Google accounts that will be authorized to test your application while it is in sandbox mode. Click the “ADD USERS” button and input the email addresses. Click the “SAVE AND CONTINUE” button to proceed.add the test user
  9. Click on the “Credentials” option in the left sidebar. Select the “CREATE CREDENTIALS” button and choose “OAuth client ID” from the list of options provided.
    select oauth client ID
  10. Choose “Web application” as the application type, and input a name for your app. Specify the authorized redirect URI as http://localhost:8000/api/sessions/oauth/google and click the “Create” button.
    set up the Google OAuth app for the Rust API Project

    After the client ID has been created, copy the client ID and secret from the “Credentials” page and add them to the .env file. Here’s an example .env file to guide you.

.env


GOOGLE_OAUTH_CLIENT_ID=
GOOGLE_OAUTH_CLIENT_SECRET=
GOOGLE_OAUTH_REDIRECT_URL=http://localhost:8000/api/sessions/oauth/google

GITHUB_OAUTH_CLIENT_ID=
GITHUB_OAUTH_CLIENT_SECRET=
GITHUB_OAUTH_REDIRECT_URL=http://localhost:8000/api/sessions/oauth/github

How to Obtain the GitHub OAuth Credentials

Follow these steps to register a GitHub OAuth app and obtain the Client ID and Secret.

  1. Start by signing into your GitHub account. Click on your profile picture at the top right corner and select “Settings” from the dropdown menu. You’ll be taken to the GitHub Developer Settings page.
    click on the profile photo icon to display a dropdown
  2. Scroll down to find the “Developer settings” section and click on it.
    click on the developer settings menu on the profile settings page
  3. On the Developer settings page, look for “OAuth Apps” and click on it. Under the “OAuth Apps” section, click on the “New OAuth App” button.
  4. Fill in the “Application name” and “Homepage URL” input fields with the appropriate name and URL for your app. For the “Authorization callback URL”, enter the URL where GitHub will redirect users after they’ve authorized the app.
    set up the GitHub OAuth app for the Rust API Project
    In this example, you need to enter http://localhost:8000/api/sessions/oauth/github as the redirect URL. Once you’re finished, hit the “Register application” button to create the OAuth App.
  5. Congrats, your application has been created! You’ll now be taken to the application details page where you can access the “Client ID” and generate the “Client Secret” keys.

    To generate the OAuth client secret, click on the “Generate a new client secret” button. GitHub will then prompt you to confirm your identity before the client secret is generated.

    the oauth client secret will be generated
  6. With the GitHub OAuth client secret now generated, it’s time to add it, along with the client ID, to your .env file. Check out this example .env file to guide you.

.env


GOOGLE_OAUTH_CLIENT_ID=
GOOGLE_OAUTH_CLIENT_SECRET=
GOOGLE_OAUTH_REDIRECT_URL=http://localhost:8000/api/sessions/oauth/google

GITHUB_OAUTH_CLIENT_ID=
GITHUB_OAUTH_CLIENT_SECRET=
GITHUB_OAUTH_REDIRECT_URL=http://localhost:8000/api/sessions/oauth/github

Conclusion

In conclusion, we have successfully built a frontend web application using Rust and the Yew framework that supports credential authentication and OAuth login through Google and GitHub. Additionally, we learned how to register an OAuth application on these platforms, which is a valuable skill in itself.

I hope this guide has been helpful and informative for you. If you have any questions or feedback, please don’t hesitate to leave a comment. Thank you for reading!