Last active
May 18, 2025 03:52
-
-
Save arisris/74cce117c4c7617348ff0d5fb4e170e3 to your computer and use it in GitHub Desktop.
Hanko auth provider for sveltekit, it use tailwindcss for styling
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <script module lang="ts"> | |
| import { onMount, type Snippet } from "svelte"; | |
| import { register } from "@teamhanko/hanko-elements"; | |
| import type { StateName, Hanko, User } from "@teamhanko/hanko-elements"; | |
| import { PUBLIC_HANKO_API_URL } from "$env/static/public"; | |
| import { scale } from "svelte/transition"; | |
| import { goto } from "$app/navigation"; | |
| import { page } from "$app/state"; | |
| import { browser } from "$app/environment"; | |
| let hanko = $state<Hanko>(); | |
| let user = $state<User | null>(null); | |
| let loading = $state<boolean>(true); | |
| let authDialogShow = $state<boolean>(false); | |
| let elementLoaded = $state<boolean>(false); | |
| const INIT_STATE_NAMES: StateName[] = [ | |
| "login_init", | |
| "profile_init", | |
| "registration_init", | |
| ]; | |
| const getAuth = () => { | |
| if (!browser) | |
| throw new Error("Hanko auth is only available in the browser context"); | |
| return hanko; | |
| }; | |
| const toggleAuthDialog = () => { | |
| if (elementLoaded) { | |
| elementLoaded = false; | |
| } | |
| authDialogShow = !authDialogShow; | |
| }; | |
| const signOut = (to?: string) => { | |
| const auth = getAuth(); | |
| if (!auth) return; | |
| auth.logout().then(() => { | |
| if (authDialogShow) authDialogShow = false; | |
| goto(to || "/"); | |
| }); | |
| }; | |
| const revalidate = async () => { | |
| const auth = getAuth(); | |
| if (!auth) return; | |
| try { | |
| loading = true; | |
| const token = auth.getSessionToken() as string | undefined; | |
| if (token && token.length > 0) { | |
| user = await auth.getUser(); | |
| } else { | |
| user = null; | |
| } | |
| } catch (e) { | |
| console.error(e); | |
| user = null; | |
| } finally { | |
| loading = false; | |
| } | |
| }; | |
| export { | |
| authButton, | |
| revalidate, | |
| getAuth, | |
| signOut, | |
| toggleAuthDialog, | |
| hankoAuthProfileSnippet, | |
| hankoAuthSnippet, | |
| }; | |
| </script> | |
| <script lang="ts"> | |
| let { | |
| children, | |
| }: { | |
| children: Snippet; | |
| } = $props(); | |
| const afterStateChange = (state: { state: { name: StateName } }) => { | |
| if (INIT_STATE_NAMES.includes(state.state.name)) { | |
| elementLoaded = true; | |
| } | |
| }; | |
| $effect(() => { | |
| if (hanko) { | |
| const u1 = hanko.onSessionCreated(revalidate); | |
| const u2 = hanko.onUserLoggedOut(revalidate); | |
| const u3 = hanko.onSessionExpired(revalidate); | |
| const u4 = hanko.onUserDeleted(revalidate); | |
| const u5 = hanko.onAfterStateChange(afterStateChange); | |
| return () => { | |
| u1(); | |
| u2(); | |
| u3(); | |
| u4(); | |
| u5(); | |
| }; | |
| } | |
| }); | |
| onMount(() => { | |
| register(PUBLIC_HANKO_API_URL, { | |
| shadow: true, | |
| sessionCheckInterval: 1000 * 60, | |
| }) | |
| .then((e) => (hanko = e.hanko)) | |
| .then(revalidate) | |
| .catch((e) => { | |
| console.error(e); | |
| hanko = undefined; | |
| }); | |
| }); | |
| </script> | |
| {#snippet hankoAuthSnippet()} | |
| <hanko-auth></hanko-auth> | |
| {/snippet} | |
| {#snippet hankoAuthProfileSnippet()} | |
| <hanko-profile></hanko-profile> | |
| {/snippet} | |
| {#snippet authButton()} | |
| {#if loading} | |
| <button type="button" class="btn btn-primary btn-block btn-sm" disabled> | |
| <span class="loading loading-xs loading-bars"></span> | |
| <span class="sr-only">Auth Loading</span> | |
| </button> | |
| {:else if user} | |
| <button | |
| type="button" | |
| title="Profile" | |
| class="group btn btn-secondary btn-block btn-sm" | |
| onclick={toggleAuthDialog} | |
| > | |
| <span>Profile</span> | |
| <span class="iconify ri--user-line size-4 group-hover:scale-105"></span> | |
| </button> | |
| {:else} | |
| <button | |
| type="button" | |
| title="Sign In" | |
| class="group btn btn-primary btn-block btn-sm" | |
| onclick={toggleAuthDialog} | |
| > | |
| <span>Sign In</span> | |
| <span class="iconify ri--login-box-line size-4 group-hover:scale-105" | |
| ></span> | |
| </button> | |
| {/if} | |
| {/snippet} | |
| {@render children()} | |
| {#if hanko && authDialogShow} | |
| <dialog out:scale={{ duration: 200 }} class="modal modal-open"> | |
| <div | |
| class="relative flex flex-col items-center justify-center p-4 w-full max-w-[410px] bg-base-100 rounded-md" | |
| > | |
| {#if user} | |
| {@render hankoAuthProfileSnippet()} | |
| {#if elementLoaded} | |
| <div class="flex flex-col gap-y-1 mt-auto w-full"> | |
| {#if !page.url.pathname.startsWith("/manage")} | |
| <button | |
| type="button" | |
| class="btn btn-primary btn-block btn-sm" | |
| onclick={() => { | |
| authDialogShow = false; | |
| setTimeout(() => goto("/manage"), 300); | |
| }} | |
| > | |
| <span class="iconify ri--dashboard-line size-4"></span> | |
| <span>Manage</span> | |
| </button> | |
| {/if} | |
| <button | |
| type="button" | |
| title="Sign Out" | |
| class="group btn btn-secondary btn-block btn-sm" | |
| onclick={() => signOut()} | |
| > | |
| <span | |
| class="iconify ri--logout-box-line size-4 group-hover:scale-105" | |
| ></span> | |
| <span>Sign Out</span> | |
| </button> | |
| </div> | |
| {/if} | |
| {:else} | |
| {@render hankoAuthSnippet()} | |
| {/if} | |
| {#if !elementLoaded} | |
| <spam class="text-sm">Loading...</spam> | |
| {/if} | |
| <form method="dialog" class="absolute top-1 right-1"> | |
| <button | |
| class="tooltip tooltip-bottom btn btn-ghost btn-circle" | |
| tabindex="-1" | |
| data-tip="Close" | |
| onclick={toggleAuthDialog} | |
| > | |
| <span class="iconify ri--close-line size-6"></span> | |
| <span class="sr-only">Close</span> | |
| </button> | |
| </form> | |
| </div> | |
| </dialog> | |
| {/if} | |
| <style lang="postcss"> | |
| @reference "$lib/app.css"; | |
| hanko-auth, | |
| hanko-profile { | |
| --background-color: var(--color-base-100); | |
| --color: var(--color-base-content); | |
| --color-shade-1: color-mix( | |
| in oklab, | |
| var(--color) 50%, | |
| var(--color-base-content) 10% | |
| ); | |
| --color-shade-2: var(--color-base-300); | |
| --brand-color: var(--color-primary); | |
| --brand-color-shade-1: color-mix( | |
| in oklab, | |
| var(--brand-color), | |
| var(--color-primary-content) 5% | |
| ); | |
| --brand-contrast-color: color-mix( | |
| in oklab, | |
| var(--color), | |
| var(--color-primary-content) 5% | |
| ); | |
| --link-color: var(--color-primary); | |
| --error-color: var(--color-error); | |
| } | |
| hanko-profile::part(container) { | |
| --container-padding: 24px 6px; | |
| } | |
| hanko-profile::part(headline1) { | |
| --headline1-font-size: 18px; | |
| } | |
| </style> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment