DEV Community

s2ahil
s2ahil

Posted on • Edited on

Hanko auth in react and Express.js .

Here you can see how to integrate auth using hanko .

Hanko : Hanko is a lightweight, open source user authentication solution to quickly setup in your project ( trust me i tried it is very easy to setup. )

In this post, i will show how I implemented authentication using hanko in a react frontend and express backend .

I made a Text summarizer ai .

To see my full code you can go to my github :
frontend:"https://github.com/s2ahil/Hanko-auth-frontend-"
backend :" https://github.com/s2ahil/Hanko-auth-backend"

I will be showing you the important codes for authentication :

*Already docs are available from Hanko *:
https://docs.hanko.io/quickstarts/frontend/react

React codes:
Here is the login code :

import React from 'react'
import { register, Hanko } from "@teamhanko/hanko-elements";
import { useNavigate } from "react-router-dom";
import { useEffect, useMemo, useCallback } from 'react';


const hankoApi = import.meta.env.REACT_APP_HANKO_API
console.log(hankoApi)


export const Login = () => {
  const navigate = useNavigate();
  const hanko = useMemo(() => new Hanko(hankoApi), []);

  const redirectAfterLogin = useCallback(() => {
    navigate("/MainPage");
  }, [navigate]);

  useEffect(
    () =>
      hanko.onAuthFlowCompleted(() => {
        redirectAfterLogin();
      }),
    [hanko, redirectAfterLogin]
  );

  useEffect(() => {
    register(hankoApi).catch((error) => {
      console.log(error)
      // handle error
    });


    if (hanko.session.isValid() === true) {
      navigate('/MainPage')
    }
  }, []);


  return (
    <div className='flex min-h-screen justify-center items-center'>
      <div className='w-[400px] p-10 border rounded-xl shadow-md '>
        <hanko-auth />
      </div>

      {/* <hanko-auth api={hankoApi} /> */}
    </div>
  )
}

Enter fullscreen mode Exit fullscreen mode

Here is logout code :

import React,{  useState,useEffect} from 'react'
import { useNavigate } from "react-router-dom";
import { Hanko } from "@teamhanko/hanko-elements";

const hankoApi = import.meta.env.REACT_APP_HANKO_API

export const Logout = () => {

    const navigate = useNavigate();
    const [hanko, setHanko] = useState(null)

    useEffect(() => {
        import("@teamhanko/hanko-elements").then(({ Hanko }) =>
            setHanko(new Hanko(hankoApi ?? ""))
        );
    }, []);

    const logout = async () => {
        try {
            await hanko?.user.logout();
            navigate("/");
        } catch (error) {
            console.error("Error during logout:", error);
        }
    };

    return (<>


        <button  className=" px-4 py-2 mt-2 bg-blue-500 text-white rounded hover:bg-green-600" onClick={async()=>await logout()}>logout</button>
    </>
    )
}


Enter fullscreen mode Exit fullscreen mode

Here is the profile code :

import React, { useEffect, useState } from 'react';
import { register } from "@teamhanko/hanko-elements";
import { useNavigate } from "react-router-dom";
import { Hanko } from "@teamhanko/hanko-elements";


const hankoApi = import.meta.env.REACT_APP_HANKO_API;
const hanko = new Hanko(import.meta.env.REACT_APP_HANKO_API);

export const Profile = () => {
  const navigate = useNavigate();
  const [showProfilePage, setShowProfile] = useState(false);

  useEffect(() => {
    async function fetchData() {

      await register(hankoApi);






    }

    fetchData();
  }, []);

  return (<>


    <button
      className="px-4 py-2 mb-2 bg-blue-500 text-white rounded hover:bg-green-600"
      onClick={() => setShowProfile(!showProfilePage)}
    >
      Profile Setting
    </button>

    {
      showProfilePage && (
        <div className='w-[400px] p-3 border rounded-xl shadow-md bg-[#FFFFFF] '>


          <hanko-profile />
        </div>

      )
    }

  </>);
}




Enter fullscreen mode Exit fullscreen mode

Here is the final main page :

import React, { useEffect, useState } from 'react';
import { Profile } from './Profile';
import { Logout } from './Logout';
import axios from 'axios';
import { TypeAnimation } from 'react-type-animation';
import { Hanko } from "@teamhanko/hanko-elements";
import { useNavigate } from "react-router-dom";

export const MainPage = () => {


    const navigate = useNavigate();
    const [text, setText] = useState('');
    const [showMainPage, setShowMainPage] = useState(false);
    const [response, setResponse] = useState('');
    const [error, setError] = useState(null); // State variable for error message
    const [loading, setLoading] = useState(false);
    const hanko = new Hanko(import.meta.env.REACT_APP_HANKO_API);
    const send = () => {
        setLoading(true);
        setError(null); // Clear previous error message, if any
console.log()
        axios.post("http://localhost:3000/summarize", { text_to_summarize: text }, { withCredentials: true })
            .then(res => {
                console.log('response', JSON.stringify(res.data));
                setResponse(res.data);
            })
            .catch(error => {
                console.error('Error:', error);
                setError("An error occurred. Please try again."); // Set the error message
            })
            .finally(() => {
                setLoading(false);
            });
    }


    useEffect(()=>{
            if (hanko.session.isValid() === false) {
        navigate('/login')
      }
    })

    return (
        <div className="p-8 bg-gray-100">
            <div className='relative space-x-10'>
                <Profile />
                <Logout />
            </div>

            <center><div className='m-4 mb-8 text-xl font-bold leading-none tracking-tight text-gray-900 md:text-2xl lg:text-3xl dark:text-black'>AI TEXT SUMMARIZER</div></center>

            <div>
                <div className="grid grid-cols-1 lg:grid-cols-2  gap-4">
                    <div className='flex flex-col'>
                        <textarea
                            className="mt-5 p-2 border border-gray-300 rounded min-h-[20rem] w-full"
                            placeholder="Enter text to summarize"
                            onChange={(e) => setText(e.target.value)}
                            value={text}
                        />
                        <div className='max-w-[20rem] mx-auto'>
                            <button
                                className={`px-5 py-3 mt-2 w-full bg-blue-500 text-white rounded-full hover:bg-green-600 ${loading ? 'cursor-not-allowed' : ''}`}
                                onClick={send}
                                disabled={loading}
                            >
                                {loading ? 'Loading...' : 'Send'}
                            </button>
                        </div>
                    </div>
                    <div>
                        {error ? ( // Check for error message
                            <div className="mt-4 text-red-800">
                                <strong>Error:</strong>
                                <p>{error}</p>
                            </div>
                        ) : response === '' ? (
                            <div className='mt-5'>
                                <div className='flex flex-col'>
                                    <p className="mb-4 text-4xl font-extrabold leading-none tracking-tight text-gray-900 md:text-4xl lg:text-5xl dark:text-black">
                                        {loading ? 'Loading...' : <> <TypeAnimation
                                            sequence={[
                                                // Same substring at the start will only be typed out once, initially
                                                'Try typing something to see the summarized text',
                                                2000, // wait 1s before replacing "Mice" with "Hamsters"
                                                'Our AI can summarize anything',
                                                2000,

                                            ]}
                                            wrapper="span"
                                            speed={50}

                                            repeat={Infinity}
                                        /></> }
                                    </p>
                                    {loading ? (
                                        <div className="w-20 h-20 mx-auto">
                                            <div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-blue-500"></div>
                                        </div>
                                    ) : (
                                        <div className=''>
                                        <iframe
                                            height="250"
                                            width="250"
                                            src="https://giphy.com/embed/26AHONQ79FdWZhAI0"
                                            className="giphy-embed "
                                        ></iframe>
                                        </div>
                                    )}
                                </div>
                            </div>
                        ) : (
                            <div className="mt-4 text-gray-800">
                                <strong>Summary:</strong>
                                <p>{response}</p>
                            </div>
                        )}
                    </div>
                </div>
            </div>
        </div>
    );
};

Enter fullscreen mode Exit fullscreen mode

Express.js codes :
Here is backend code :"

const express = require('express');
const bodyParser = require('body-parser');
const cors = require("cors");
const cookieParser = require("cookie-parser");
const jose = require("jose"); // Import the jose library
const dotenv = require("dotenv");
const summarizeText = require('./summarize')
dotenv.config()

const app = express();
app.use(cors({ credentials: true, origin: true }));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }))
app.use(cookieParser());

console.log(process.env.HANKO_API_URL)


async function Middleware(req, res, next) {


  const token = req.cookies.hanko;

  console.log("token", token)

  if (!token) {
    return res.status(401).json({ error: "Unauthorized" });
  }


  const hankoApi = process.env.HANKO_API_URL

  const JWKS = jose.createRemoteJWKSet(
    new URL(`${hankoApi}/.well-known/jwks.json`)
  );

  console.log(JWKS)
  try {
    // Verify the token using the JWKS
    console.log("aya 1")
    const verifiedJWT = await jose.jwtVerify(token, JWKS);

    console.log("aya 2")
    // console.log(verifiedJWT)
    req.auth = verifiedJWT; // Attach the authentication data to the request object
    next();
  } catch (error) {
    return res.status(401).json({ error: "Unauthorized" });
  }
}

app.get("/protected", (req, res) => {
  // Only users with a valid Hanko token will reach this route
  if (req.auth) {
    console.log(req.auth)
    res.sendStatus(200);
  } else {
    res.status(401).json({ error: "Unauthorized" });
  }
});

app.post("/summarize", Middleware, (req, res) => {


  if (req.auth) {

    const text = req.body.text_to_summarize;
    console.log(text)
    summarizeText(text)
      .then(response => {
        res.send(response); // Send the summary text as a response to the client
      })
      .catch(error => {
        console.log(error.message);
      });
  }else{
    res.status(401).json({ error: "Unauthorized" });
  }
  // get the text_to_summarize property from the request body

});


app.get('/', (req, res) => {
  res.send("hello");
})









app.listen(3000, () => {
  console.log("Server started at 3000");
});

Enter fullscreen mode Exit fullscreen mode

Note : somehow while deploying req.cookies was not showing cookies from browser , but it was working fine in local host . you can check from your side also .

Finally, i deployed my frontend to : Vercel
Express backend to : render.com

Full Live Working project Link :"https://hanko-auth-frontend.vercel.app/"

Top comments (1)

Collapse
 
dotenv profile image
Dotenv

💛🌴