Skip to content

Commit

Permalink
adding basic server/ folder for server component utils
Browse files Browse the repository at this point in the history
  • Loading branch information
jowo-io committed Dec 11, 2023
1 parent 88bf123 commit bc245ca
Show file tree
Hide file tree
Showing 12 changed files with 222 additions and 38 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ yarn-error.log*

# build
/main
/server
/hooks
/utils
/generators
Expand Down
2 changes: 2 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Below is a TODO list for further development of `next-auth-lightning-provider`.

### Alpha

- consider ssr pages router (getServerSideProps)
- consider using next router for redirect instead of window navigator
- `Response body object should not be disturbed or locked` in `ui-app-router` example create request
- test deploy all the various example apps
- ar-generic
Expand Down
17 changes: 5 additions & 12 deletions examples/ui-app-router/app/components/LightningAuth.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,14 @@
"use client";

import { useLightningAuth } from "next-auth-lightning-provider/hooks";
import { useLightningPolling } from "next-auth-lightning-provider/hooks";
import { LightningAuthClientSession } from "next-auth-lightning-provider/server";

export default function LightningAuth({
redirectUri,
state,
session,
}: {
redirectUri: string;
state: string;
session: LightningAuthClientSession;
}) {
const { lnurl, qr, button } = useLightningAuth({ redirectUri, state });

if (!lnurl) {
return (
<div style={{ textAlign: "center", color: "black" }}>loading...</div>
);
}
const { lnurl, qr, button } = useLightningPolling(session);

return (
<div
Expand Down
34 changes: 16 additions & 18 deletions examples/ui-app-router/app/signin/page.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,26 @@
"use client";

import { useSearchParams } from "next/navigation";

import LightningAuth from "@/app/components/LightningAuth";

export default function SignIn() {
const searchParams = useSearchParams();
const redirectUri = searchParams?.get("redirect_uri");
const state = searchParams?.get("state");
import { createLightningAuth } from "next-auth-lightning-provider/server";

export default async function SignIn({
searchParams,
}: {
searchParams: Record<string, string | string[]>;
}) {
let session, error;
try {
session = await createLightningAuth(searchParams);
} catch (e: any) {
error = e.message || "Something went wrong";
}

if (!redirectUri || !state) {
return (
<div style={{ textAlign: "center", color: "red" }}>
Missing query params
</div>
);
if (error || !session) {
return <div style={{ textAlign: "center", color: "red" }}>{error}</div>;
}

return (
<div>
<LightningAuth
redirectUri={redirectUri as string}
state={state as string}
/>
<LightningAuth session={session} />
</div>
);
}
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
"hooks/**/*.js",
"utils/**/*.d.ts",
"utils/**/*.js",
"server/**/*.d.ts",
"server/**/*.js",
"main/**/*.d.ts",
"main/**/*.js",
"index.d.ts",
Expand All @@ -44,7 +46,7 @@
"main": "./index.js",
"scripts": {
"build": "tsc && tsc-alias",
"clean": "rm -rf main hooks utils generators index.d.ts index.d.ts.map index.js",
"clean": "rm -rf main server hooks utils generators index.d.ts index.d.ts.map index.js",
"dev": "concurrently \"tsc -w\" \"tsc-alias -w\"",
"test": "jest --watch",
"prebuild": "npm run clean && jest"
Expand Down
1 change: 1 addition & 0 deletions src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from "./useLightningAuth";
export * from "./useLightningPolling";
16 changes: 10 additions & 6 deletions src/hooks/useLightningAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const maxNetworkRequestsFailures = 3;
* A React hook that, on mount, will trigger an API request and create a new Lightning auth session.
* Thereafter, it'll poll the API and checks if the Lightning auth QR has been scanned.
* If enough time elapses without a sign in attempt, the Lightning auth session will be refreshed.
* Once a success status is received from polling, the user will be redirected to the `redirectUri`.
* Once a success status is received from polling, the user will be redirected to the `next-auth` redirect url.
*
* @param {String} redirectUri - the `redirect_uri` query param generated by `next-auth`
* @param {String} state - the `state` query param generated by `next-auth`
Expand All @@ -28,11 +28,11 @@ export function useLightningAuth({
redirectUri: string;
state: string;
}): {
lnurl: string | null;
lnurl: string;
qr: string;
button: string;
} {
const [lnurl, setUrl] = useState<string | null>(null);
const [lnurl, setUrl] = useState<string>("");

useEffect(() => {
let session: {
Expand Down Expand Up @@ -145,13 +145,13 @@ export function useLightningAuth({
}

session = d;
if (!session || !session.lnurl) return;
if (!session) return;

// setup intervals and create first qr code
pollTimeoutId = setTimeout(poll, session?.pollInterval);
createIntervalId = setInterval(create, session?.createInterval);

setUrl(session?.lnurl || null);
setUrl(session.lnurl);
})
.catch((e) => {
if (!createController.signal.aborted) {
Expand All @@ -167,5 +167,9 @@ export function useLightningAuth({

const { qr, button } = formatLightningAuth(lnurl);

return { lnurl, qr, button };
return {
lnurl,
qr,
button,
};
}
125 changes: 125 additions & 0 deletions src/hooks/useLightningPolling.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
"use client";

import { useEffect } from "react";
import { useRouter } from "next/navigation";

import { hardConfig } from "../main/config/hard";
import { LightningAuthClientSession } from "../server";
import { formatLightningAuth } from "../utils/lnurl";

const maxNetworkRequestsFailures = 3;

/**
* 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.
*
* @param {LightningAuthClientSession} 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: LightningAuthClientSession): {
lnurl: string | null;
qr: string;
button: string;
} {
const router = useRouter();

useEffect(() => {
let pollTimeoutId: NodeJS.Timeout | undefined;
let createIntervalId: NodeJS.Timeout | undefined;
let networkRequestCount: number = 0;
let errorUrl: string | undefined;
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: any) {
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 };
}
53 changes: 53 additions & 0 deletions src/server/createLightningAuth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { formatLightningAuth } from "../utils/lnurl";
import { hardConfig } from "../main/config";

export type LightningAuthClientSession = {
data: {
k1: string;
lnurl: string;
};
intervals: {
poll: number;
create: number;
};
query: {
state: string;
redirectUri: string;
};
};

export default async function createLightningAuth(
searchParams: any
): Promise<LightningAuthClientSession> {
const siteUrl = searchParams.client_id;
const state = searchParams.state;
const redirectUri = searchParams.redirect_uri;
if (!redirectUri || !state) {
throw new Error("Missing query params");
}

const params = new URLSearchParams({ state });
const response = await fetch(siteUrl + hardConfig.apis.create, {
method: "POST",
headers: { "content-type": "application/x-www-form-urlencoded" },
body: params,
cache: "no-cache",
});
const data = await response.json();

if (!data.lnurl) {
throw new Error("Missing lnurl");
}

return {
data: {
k1: data.k1,
lnurl: data.lnurl,
},
intervals: {
poll: data.pollInterval,
create: data.createInterval,
},
query: { state, redirectUri },
};
}
4 changes: 4 additions & 0 deletions src/server/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export {
default as createLightningAuth,
LightningAuthClientSession,
} from "./createLightningAuth";
2 changes: 1 addition & 1 deletion src/utils/lnurl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { hardConfig } from "../main/config/hard";

export function formatLightningAuth(lnurl?: string | null) {
if (!lnurl) {
return { lnurl, qr: "", button: "" };
return { lnurl: "", qr: "", button: "" };
}

const qr = `${hardConfig.apis.qr}/${lnurl}`;
Expand Down
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"main",
"generators",
"hooks",
"server",
"utils",
"examples",
"node_modules"
Expand Down

0 comments on commit bc245ca

Please sign in to comment.