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.
112 lines (111 loc) • 4.72 kB
JavaScript
"use client";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
import { hardConfig } from "../main/config/hard.js";
import { formatLightningAuth } from "../utils/lnurl.js";
import { maxNetworkRequestsFailures } from "./constants.js";
/**
* A React hook that, on mount, will poll the API and checks if the Lightning auth QR has been scanned.
* If enough time elapses without a sign in attempt, the page will be refreshed.
* Once a success status is received from polling, 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 `createLightningAuth` method
*
* @returns {Object}
* @returns {String} lnurl - the raw LNURL, should be made available for copy-pasting
* @returns {String} qr - a url pointing the lnurl-auth QR Code image, should be used in the src prop of img tags
* @returns {String} button - a deep-link that will open in Lightning enabled wallets, should be used in the href prop of anchor tags
*/
export function useLightningPolling(session) {
const router = useRouter();
useEffect(() => {
let pollTimeoutId;
let createIntervalId;
let networkRequestCount = 0;
let errorUrl;
const pollController = new AbortController();
// cleanup when the hook unmounts of polling is successful
const cleanup = () => {
clearTimeout(pollTimeoutId);
clearInterval(createIntervalId);
pollController.abort();
};
// redirect user to error page if something goes wrong
function error(e) {
console.error(e);
if (errorUrl) {
window.location.replace(errorUrl);
}
else {
// 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()}`);
}
}
// poll the api to see if the user has successfully authenticated
function poll() {
if (!session?.data?.k1)
return;
const k1 = session.data.k1;
const params = new URLSearchParams({ k1 });
return fetch(hardConfig.apis.poll, {
method: "POST",
headers: { "content-type": "application/x-www-form-urlencoded" },
body: params,
cache: "default",
signal: pollController.signal,
})
.then((r) => {
if (r.status === 410) {
// if resource not found throw error immediately,
// this means the user's auth session has been deleted.
networkRequestCount = maxNetworkRequestsFailures;
}
return r.json();
})
.catch((e) => {
// if there are more than X network errors, then trigger redirect
networkRequestCount++;
if (networkRequestCount >= maxNetworkRequestsFailures) {
pollTimeoutId = setTimeout(poll, session.intervals.poll);
throw e;
}
})
.then((d) => {
if (d && d.error) {
if (d.url)
errorUrl = d.url;
throw new Error(d.message || d.error);
}
if (d)
networkRequestCount = 0;
if (d && d.message)
throw new Error(d.message);
pollTimeoutId = setTimeout(poll, session.intervals.poll);
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 (!pollController.signal.aborted) {
error(e);
}
});
}
function create() {
router.refresh();
}
pollTimeoutId = setTimeout(poll, session.intervals.poll);
createIntervalId = setInterval(create, session.intervals.create);
return () => cleanup();
}, []);
const { lnurl, qr, button } = formatLightningAuth(session.data.lnurl);
return { lnurl, qr, button };
}