UNPKG

next-auth-pubkey

Version:

A light-weight Lightning and Nostr auth provider for your Next.js app that's entirely self-hosted and plugs seamlessly into the next-auth framework.

120 lines (119 loc) 4.69 kB
"use client"; import { useState, useRef, useEffect } from "react"; import { useRouter } from "next/navigation"; import { hardConfig } from "../main/config/hard.js"; /** * A React hook that, on mount, will trigger any installed Nostr extensions or return an error. * Once a success Nostr extension event is received from the extension the user will be * redirected to the `next-auth` redirect url. * * This hook is designed for use in both the pages router and the app router. * * @param {object} session - a session object generated by invoking the `createNostrAuth` method * * @returns {Object} * @returns {String} error - an error message or an empty string * @returns {Function} retry - a function used to retry opening the nostr extension */ export function useNostrExtension(session) { const retryRef = useRef(null); const [error, setError] = useState(""); const [isRetry, setRetry] = useState(true); const router = useRouter(); useEffect(() => { const callbackController = new AbortController(); // cleanup when the hook unmounts of callbacking is successful const cleanup = () => { callbackController.abort(); }; // redirect user to error page if something goes wrong function error(e) { console.error(e); // if no errorUrl exists send to defaul `next-auth` error page const params = new URLSearchParams(); params.append("error", "OAuthSignin"); window.location.replace(`/api/auth/error?${params.toString()}`); } // callback the api to authenticate the user function callback(event) { if (!session?.data?.k1) return; const k1 = session.data.k1; const params = new URLSearchParams({ event: JSON.stringify(event) }); return fetch(hardConfig.apis.callback, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body: params, cache: "default", signal: callbackController.signal, }) .then(function (r) { return r.json(); }) .then(function (d) { if (d && d.success) { cleanup(); let url = new URL(session.query.redirectUri); url.searchParams.append("state", session.query.state); url.searchParams.append("code", k1); router.replace(url.toString()); } }) .catch((e) => { if (!callbackController.signal.aborted) { error(e); } }); } function callWithTimeout(targetFunction, timeoutMs) { return new Promise((resolve, reject) => { Promise.race([ targetFunction(), new Promise((resolve, reject) => setTimeout(() => reject(new Error("timed out after " + timeoutMs + " ms waiting for extension")), timeoutMs)), ]) .then(resolve) .catch(reject); }); } // trigger the nostr extension / handle errors on failure function triggerExtension(k1) { if (!window.nostr) { setRetry(false); setError("nostr extension not found"); return; } return callWithTimeout(() => window.nostr.signEvent({ kind: 22242, created_at: Math.floor(Date.now() / 1000), tags: [["challenge", k1]], content: "Authentication", }), 5000) .then((event) => { if (event) { callback(event); } else { setRetry(true); setError("extension returned empty event"); } }) .catch((e) => { const message = e instanceof Error ? e.message : "nostr extension failed to sign event"; if (message === "window.nostr call already executing") { return; } setRetry(true); setError(message); }); } retryRef.current = () => triggerExtension(session?.data?.k1); retryRef.current(); return () => cleanup(); }, []); return { error, retry: isRetry && error ? () => retryRef?.current?.() : null, }; }