diff --git a/quickstarts/fullstack/remix.mdx b/quickstarts/fullstack/remix.mdx index d9eea52..054bcd8 100644 --- a/quickstarts/fullstack/remix.mdx +++ b/quickstarts/fullstack/remix.mdx @@ -4,28 +4,51 @@ title: Integrate Hanko with Remix description: Learn how to quickly add authentication and user profile in your Remix app using Hanko. --- -## Install `@teamhanko/hanko-elements` +## Create a Remix application +Run the following command to [create a new Remix application](https://remix.run/docs/en/main/start/quickstart): + + +```bash terminal +npx create-remix@latest +``` + + +## Install `@teamhanko/hanko-elements` Once you've initialized your Remix app, installing hanko-elements provides you with access to the prebuilt components: `hanko-auth` and `hanko-profile`. - + ```bash npm +cd project-name npm install @teamhanko/hanko-elements ``` ```bash pnpm +cd project-name pnpm add @teamhanko/hanko-elements ``` ```bash bun +cd project-name bun add @teamhanko/hanko-elements ``` ```bash yarn +cd project-name yarn add @teamhanko/hanko-elements ``` + +## Setup your Hanko project +Go to the [Hanko console](https://cloud.hanko.io/) and [create a project for this application.](/setup-hanko-cloud) + + + During creation make sure to input the URL you will be developing on as the `APP URL`.\ + (Most likely http://localhost:5173/) + + + ## Add the Hanko API URL Retrieve the API URL from the [Hanko console](https://cloud.hanko.io/) and place it in your `.env` file. @@ -39,6 +62,7 @@ HANKO_API_URL=https://f4****-4802-49ad-8e0b-3d3****ab32.hanko.io backend. + ## Importing and exporting Hanko functions in a `.client.ts` file Since the `@teamhanko/hanko-elements` package is designed to work on the client side, you'll need to import the Hanko functions into a `.client.ts` file. This file will then export them, making them available for use throughout your application. @@ -49,289 +73,278 @@ import { register, Hanko } from "@teamhanko/hanko-elements"; export { register, Hanko }; ``` +## Create Hanko components +Create a folder called `components` and create two files in it, `HankoAuth.tsx` and `HankoProfile.jsx`. -## Add `` component +### Hanko Auth -The `` web component adds a login interface to your app. The following code example defines a `HankoAuth` component in the `login` route. +Now lets setup the `HankoAuth.tsx` file to create a functioning login page. +Here we subscribe to the `onSessionCreated` [event](/guides/hanko-elements/using-frontend-sdk#events), this triggers when a user successfully logs in. You can use these event to perform any desired action (e.g. redirect to your dashboard). -It uses the register function from `hanko.client.ts` to register `` component with the brower's [CustomElementRegistry](https://developer.mozilla.org/de/docs/Web/API/CustomElementRegistry). When the authentication flow is completed, a callback is set up to redirect the user to the dashboard. +[For more information please refer to the Auth Component Page.](/guides/hanko-elements/auth-component) -```tsx routes/login.tsx +```jsx components/HankoAuth.tsx import { useNavigate, useLoaderData } from "@remix-run/react"; - import { Suspense, useCallback, useEffect, useState } from "react"; - -import { register, type Hanko } from "~/utils/hanko.client"; +import { register, type Hanko } from "../utils/hanko.client"; export const loader = () => { return { hankoUrl: process.env.HANKO_API_URL }; }; - const HankoAuth = () => { - const [hanko, setHanko] = useState(); - const navigate = useNavigate(); - - const data = useLoaderData(); - const hankoUrl = data.hankoUrl || ''; - - const redirectAfterLogin = useCallback(() => { - navigate("/dashboard"); - }, [navigate]); + const [hanko, setHanko] = useState(); + const navigate = useNavigate(); + + const data = useLoaderData(); + const hankoUrl = data.hankoUrl || ''; + + const redirectAfterLogin = useCallback(() => { + navigate("/dashboard");//Path user gets navigated to once they log in + }, [navigate]); + + useEffect(() => { + if (hanko) { + hanko.onSessionCreated(() => { + //Succesfully Logged In + redirectAfterLogin(); + }); + } + }, [hanko, redirectAfterLogin]); + + useEffect(() => { + register(hankoUrl) + .catch((error: Error) => { + console.error(error.message); + }) + .then((result) => { + if (result) { + setHanko(result.hanko); + } + }); + }, [hankoUrl]); + + return ( +
+ + + +
+ ); +}; - useEffect(() => { - if (hanko) { - hanko.onSessionCreated(() => { - redirectAfterLogin(); - }); - } - }, [hanko, redirectAfterLogin]); +export default HankoAuth; +``` - useEffect(() => { - register(hankoUrl) - .catch((error: Error) => { - console.error(error.message); - }) - .then((result) => { - if (result) { - setHanko(result.hanko); - } - }); - }, [hankoUrl]); - - return ( -
- - - -
- ); -}; -const Login = () => { - return ( -
- -
- ) -} +### Hanko profile -export default Login -``` +After setting up the HankoAuth let's set up the `HankoProfile.jsx` file to create an interface where users can +manage their `Email Addresses` and credentials. -By now, your sign-up and sign-in features should be working. You should see an interface similar to this πŸ‘‡ +[For more information please refer to the Profile Component Page.](/guides/hanko-elements/profile-component) - -sign up - +```jsx components/HankoProfile.tsx +import { useLoaderData } from "@remix-run/react"; +import { Suspense, useEffect, } from "react"; +import { register } from "../utils/hanko.client"; +export const loader = () => { + return { hankoUrl: process.env.HANKO_API_URL }; +}; -## Add `` component +const HankoProfile = () => { + const data = useLoaderData(); + const hankoUrl = data.hankoUrl || ''; -The `` component provides an interface, where users can manage their email addresses and passkeys. + useEffect(() => { + register(hankoUrl).catch((error) => { + // handle error + console.log(error); + }); + }); + + return( + + + + ) +} + +export default HankoProfile; +``` +## Setup your routes -```tsx app/routes/dashboard.tsx -import { useLoaderData, useNavigate } from "@remix-run/react"; +Let's setup our routes in the `/routes` folder.\ +For our example we will create `_index.tsx` and `dashboard.tsx` -import { Suspense, useEffect, useState } from "react"; -import { type Hanko, register } from "~/utils/hanko.client"; +```jsx routes/_index.tsx +import HankoAuth from "../components/HankoAuth"; export const loader = () => { - return { hankoUrl: process.env.HANKO_API_URL }; + return { hankoUrl: process.env.HANKO_API_URL }; }; -function HankoProfile() { - let data = useLoaderData(); - let hankoUrl = data.hankoUrl || ''; +export default function Index() { + return ( + <> + + + ); +} +``` - const [hanko, setHanko] = useState(); - const navigate = useNavigate(); +and +```jsx routes/dashboard.tsx +import HankoProfile from "../components/HankoProfile"; - useEffect(() => { - register(hankoUrl) - .catch((error: Error) => { - console.error(error.message); - }) - .then((result) => { - if (result) { - setHanko(result.hanko); - } - }); - }, [hankoUrl]); - - return ( -
- - - -
- ) -} +export const loader = () => { + return { hankoUrl: process.env.HANKO_API_URL }; +}; -const Dashboard = () => { - return ( -
- -
- ) +export default function Dashboard() { + return ( + <> + + + ); } - -export default Dashboard ``` -It should look like this πŸ‘‡ +By now you should be able to go to `/` to see the ``, and to `/dashboard` to see the ``. +They should look something like thisπŸ‘‡ -profile page +
+ sign up + profile page +
+ + If `HankoAuth` is not loading please restart the application as it might've not loaded the .env correctly the first time.\ + \ + `HankoProfile` will only look like the picture while you are logged in. + ## Implement logout functionality -You can use `@teamhanko/hanko-elements` to easily manage user logouts. Below we add a logout button to the dashboard page. +You can use `@teamhanko/hanko-elements` to easily logout users. Here we will make a logout button. +Create `LogoutButton.tsx` and insert the code below. -```tsx app/routes/dashboard.tsx +```jsx components/LogoutButton.tsx import { useLoaderData, useNavigate } from "@remix-run/react"; - -import { Suspense, useEffect, useState } from "react"; -import { type Hanko, register } from "~/utils/hanko.client"; +import { useEffect, useState, } from "react"; +import { type Hanko, } from "../utils/hanko.client"; export const loader = () => { return { hankoUrl: process.env.HANKO_API_URL }; }; -function HankoProfile() { - let data = useLoaderData(); - let hankoUrl = data.hankoUrl || ''; - - const [hanko, setHanko] = useState(); - const navigate = useNavigate(); +const LogoutButton = () => { + const data = useLoaderData(); + const hankoUrl = data.hankoUrl || ''; + const navigate = useNavigate(); + const [hanko, setHanko] = useState(); + useEffect(() => { - register(hankoUrl) - .catch((error: Error) => { - console.error(error.message); - }) - .then((result) => { - if (result) { - setHanko(result.hanko); - } - }); - }, [hankoUrl]); - - // here we define the logout function + import("@teamhanko/hanko-elements").then(({ Hanko }) => + setHanko(new Hanko(hankoUrl ?? "")) + ); + }, []); + const logout = async () => { try { - await hanko?.user.logout(); - navigate("/login"); + await hanko?.logout(); + + navigate("/");//Path the user will be redirected to once logged out + return; } catch (error) { console.error("Error during logout:", error); } }; - return ( -
- - - -
- -
-
- ) -} - -const Dashboard = () => { - return ( -
- -
- ) + return ; } - -export default Dashboard + +export default LogoutButton; ``` + ## Customize component styles You can customize the appearance of `hanko-auth` and `hanko-profile` components using CSS variables and parts. Refer to our [customization guide](/guides/hanko-elements/customize-appearance). ## Securing routes -To add JWT verification with Hanko in your Remix application, we're using the [jose](https://www.npmjs.com/package/jose) library. However, you're free to choose any other suitable library. - -Below we define a `validateJwtAndFetchUserId` function which validates the JWT and fetches the user ID. This will also be used to protect routes and ensure only authenticated users can access certain parts of the application. +To secure our routes we can validate our cookie token using `hankoAPI/sessions/validate`. +Create a file called `auth.server.ts` in `/services` and add this code. ```jsx app/services/auth.server.ts import { parse } from "cookie"; -import { jwtVerify, createRemoteJWKSet } from "jose"; - -const hankoApiUrl = process.env.HANKO_API_URL; - -export async function validateJwtAndFetchUserId(request: Request) { - const cookies = parse(request.headers.get("Cookie") || ""); - const token = cookies.hanko; - - if (!token) { - return null; - } - - const JWKS = createRemoteJWKSet( - new URL(`${hankoApiUrl}/.well-known/jwks.json`) - ); - let payload; - - try { - const verifiedJWT = await jwtVerify(token, JWKS); - payload = verifiedJWT.payload; - } catch { - return null; - } - const userID = payload.sub; +export async function ValidateCurrentSession(request: Request, hankoUrl: string){ - if (!userID) { - return null; - } + const cookies = parse(request.headers.get("Cookie") || ""); + const token = cookies.hanko; - return userID; + const validationOptions = { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: `{"session_token":"${token}"}` + } + try { + const response = await fetch(hankoUrl + '/sessions/validate', validationOptions); + + if (!response.ok) throw new Error('Session validation failed'); + + const verifiedResponse = await response.json(); + + return verifiedResponse.is_valid + + } catch (error) { + console.log(error) + return false; + } } + ``` -This is how you can use it in your routes: +To protect a route using this we can validate it in the loader function. -```tsx app/routes/protected.tsx -import { type LoaderFunction, json, redirect } from '@remix-run/node'; -import { validateJwtAndFetchUserId } from '../services/auth.server'; +```tsx app/routes/dashboard.tsx +import HankoProfile from "../components/HankoProfile"; +import LogoutButton from "../components/LogoutButton"; -export let loader: LoaderFunction = async ({ request }) => { - const userID = await validateJwtAndFetchUserId(request); +import { type LoaderFunction, redirect } from "@remix-run/node"; +import { ValidateCurrentSession } from "../services/auth.server"; - if (!userID) { - return redirect('/login'); - } +export const loader: LoaderFunction = async ({request}) => { + const hankoUrl = process.env.HANKO_API_URL || ""; + const validated = await ValidateCurrentSession(request , hankoUrl); - return json({ userID }); + if(!validated){ + return redirect("/");//Path to redirect to if user is not authenticated + } + + return { hankoUrl: hankoUrl }; }; -export default function Protected() { - return ( -
-

Protected

-
- ); +export default function Dashboard() { + return ( + <> + + + + ); } ```