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.

362 lines (360 loc) 11.3 kB
import { randomBytes } from "crypto"; export function testField(received, expected, field) { const state = received?.[field] !== expected?.[field] ? "failed" : "success"; return { state, method: "get", message: `Expected 'session.${field}' to be '${expected?.[field]}', received '${received?.[field]}'.`, }; } export async function testSet(setMethod, config) { const checks = []; try { await setMethod(); checks.push({ state: "success", method: "set", message: "Invoked without throwing an error.", }); return checks; } catch (e) { if (config.flags.logs) { console.error(e); } checks.push({ state: "failed", method: "set", message: "An unexpected error occurred.", }); return checks; } } export async function testGet(expectedSession, getMethod, config) { const checks = []; try { const receivedSession = await getMethod(); checks.push({ state: "success", method: "get", message: "Invoked without throwing an error.", }); if (!receivedSession) { checks.push({ state: "failed", method: "get", message: "Session data not defined.", }); return checks; } checks.push(testField(receivedSession, expectedSession, "k1")); checks.push(testField(receivedSession, expectedSession, "state")); return checks; } catch (e) { if (config.flags.logs) { console.error(e); } checks.push({ state: "failed", method: "get", message: "An unexpected error occurred.", }); return checks; } } export async function testUpdate(expectedSession, updateMethod, getMethod, config) { const checks = []; try { const rand = randomBytes(20).toString("hex").toUpperCase(); await updateMethod(`NON_EXISTING_K1_${rand}_NON_EXISTING_K1`); if (config.flags.logs) { console.error(new Error("The storage.update method should throw an error if an existing session is not already stored under the k1.")); } checks.push({ state: "failed", method: "update", message: "Invoked on a non-existing k1 without throwing. Expected an error to be thrown when session doesn't exist.", }); return checks; } catch (e) { checks.push({ state: "success", method: "update", message: "Invoked on a non-existing k1 and threw an error.", }); } try { await updateMethod(); checks.push({ state: "success", method: "update", message: "Invoked on existing k1 without throwing an error.", }); let receivedSession; try { receivedSession = await getMethod(); } catch (e) { if (config.flags.logs) { console.error(e); } checks.push({ state: "failed", method: "get", message: "An unexpected error occurred.", }); return checks; } if (!receivedSession) { checks.push({ state: "failed", method: "get", message: "Session data not found.", }); return checks; } checks.push(testField(receivedSession, expectedSession, "k1")); checks.push(testField(receivedSession, expectedSession, "state")); checks.push(testField(receivedSession, expectedSession, "pubkey")); checks.push(testField(receivedSession, expectedSession, "sig")); checks.push(testField(receivedSession, expectedSession, "success")); return checks; } catch (e) { if (config.flags.logs) { console.error(e); } checks.push({ state: "failed", method: "update", message: "An unexpected error occurred.", }); return checks; } } export async function testDelete(deleteMethod, getMethod, config) { const checks = []; try { await deleteMethod(); checks.push({ state: "success", method: "delete", message: "Invoked without throwing an error.", }); let receivedSession; try { receivedSession = await getMethod(); } catch (e) { if (config.flags.logs) { console.error(e); } checks.push({ state: "failed", method: "get", message: "An unexpected error occurred.", }); return checks; } if (receivedSession) { checks.push({ state: "failed", method: "delete", message: "Session data was not deleted.", }); return checks; } checks.push({ state: "success", method: "delete", message: "Session data was deleted.", }); return checks; } catch (e) { if (config.flags.logs) { console.error(e); } checks.push({ state: "failed", method: "delete", message: "An unexpected error occurred.", }); return checks; } } export default async function handler({ query, cookies, url, config, }) { const checks = []; const k1 = typeof query?.k1 === "string" ? query.k1 : randomBytes(6).toString("hex"); const state = typeof query?.state === "string" ? query.state : randomBytes(6).toString("hex"); const pubkey = typeof query?.pubkey === "string" ? query.pubkey : randomBytes(6).toString("hex"); const sig = typeof query?.sig === "string" ? query.sig : randomBytes(6).toString("hex"); const qr = typeof query?.qr === "string" ? query.qr : randomBytes(32).toString("hex"); try { const setSession = { k1, state }; const updateSession = { pubkey, sig, success: true }; const setMethod = async () => await config.storage.set({ k1, data: setSession }, url, config); const getMethod = async () => await config.storage.get({ k1 }, url, config); const updateMethod = async (override) => await config.storage.update({ k1: override || k1, data: updateSession }, url, config); const deleteMethod = async () => await config.storage.delete({ k1 }, url, config); // set checks.push(...(await testSet(setMethod, config))); // get checks.push(...(await testGet(setSession, getMethod, config))); // update checks.push(...(await testUpdate({ ...setSession, ...updateSession }, updateMethod, getMethod, config))); // delete checks.push(...(await testDelete(deleteMethod, getMethod, config))); // generic throw } catch (e) { if (config.flags.logs) { console.error(e); } checks.push({ state: "failed", method: null, message: "Something went wrong running diagnostics.", }); } let name = null; try { if (config.generateName) { name = (await config.generateName(pubkey, config))?.name; if (!name) throw new Error(); } } catch (e) { name = `<div class="empty">Failed to load</div>`; } const generators = { generateQr: `<img onerror="handleError(this)" src="${config.baseUrl + config.apis.qr + "/" + qr}" width="200px" height="200px" />`, generateAvatar: config.generateAvatar ? `<img onerror="handleError(this)" src="${config.baseUrl + config.apis.avatar + "/" + pubkey}" width="200px" height="200px" />` : null, generateName: name ? `<div class="name"><span>${name}</span></div>` : null, }; return { status: 200, headers: { "content-type": "text/html", }, response: `<!DOCTYPE html><html lang="en"> <head> <meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Diagnostics</title> <style> body { display: flex; justify-content: center; font-family: ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji; } .check { display: flex; margin: 10px 0; min-width: 900px; } .state { min-width:110px } .state-failed { color: red; } .state-success { color: green; } .method { font-weight: bold; min-width:150px } .message { } .generators { text-align: center; display: flex; justify-content: center; gap: 10px; } .name { background-color: #888888; color: #eeeeee; display: flex; justify-content: center; align-items: center; width: 200px; height: 200px; padding: 10px; box-sizing: border-box; } .missing { background-color: orange; color: #eeeeee; display: flex; justify-content: center; align-items: center; width: 200px; height: 200px; padding: 10px; box-sizing: border-box; } .empty { background-color: red; color: #eeeeee; display: flex; justify-content: center; align-items: center; width: 200px; height: 200px; padding: 10px; box-sizing: border-box; } </style> </head> <body> <div> <div class="check"> <span class="state"><b>Status</b></span> <span class="method"><b>Method</b></span> <span class="message"><b>Message</b></span> </div> <hr/> ${checks .map(({ state, method, message }) => `<div class="check"> <span class="state state-${state}">${state.toUpperCase()}</span> <span class="method">${method ? `storage.${method}` : "unknown"}</span> <span class="message">${message}</span> </div>`) .join("")} <hr/> <div class="generators"> ${Object.entries(generators) .map(([key, value]) => { return ` <div id="${key}"> <pre>${key}</pre> ${value ? value : `<div class="missing">Not defined</div>`} </div>`; }) .join("")} </div> </div> <script> function handleError(target){ const a = document.createElement("span"); a.className = "empty" a.textContent = "Failed to load" target.replaceWith(a); } </script> </body> </html>`, }; }