UNPKG

@frank-auth/react

Version:

Flexible and customizable React UI components for Frank Authentication

580 lines (579 loc) 16.6 kB
import { createHybridAuthStorage as b, AuthSDK as I, NextJSCookieContext as R } from "@frank-auth/sdk"; import { NextResponse as m, NextRequest as S } from "next/server"; const w = { apiUrl: "http://localhost:8990", sessionCookieName: "frank_sid", storageKeyPrefix: "frank_auth_", publicPaths: [], privatePaths: [], authPaths: [ "/sign-in", "/sign-up", "/forgot-password", "/verify-email", "/reset-password" ], skipApiCallOnNetworkError: !1, maxRetries: 2, apiTimeout: 5e3, fallbackToLocalTokens: !0, offlineMode: !1, allPathsPrivate: !0, signInPath: "/sign-in", signUpPath: "/sign-up", afterSignInPath: "/dashboard", afterSignUpPath: "/dashboard", afterSignOutPath: "/", orgSelectionPath: "/select-organization", debug: !1, enableOrgRouting: !1, ignorePaths: [ "/api", "/_next", "/favicon.ico", "/images", "/static", "/_vercel" ], cookieOptions: { secure: process.env.NODE_ENV === "production", httpOnly: !0, sameSite: "lax", maxAge: 60 * 60 * 24 * 7 // 7 days } }; function A(s, t) { return t.some((e) => { if (e === s) return !0; if (e.endsWith("*")) { const r = e.slice(0, -1); return s.startsWith(r); } return e.startsWith("/") && e.endsWith("/") ? new RegExp(e.slice(1, -1)).test(s) : !1; }); } function O(s, t, e) { const r = s.cookies.getAll(), k = { cookies: { ...Object.fromEntries( r.map((a) => [a.name, a.value]) ), // Also provide a get method for Next.js cookie compatibility get: (a) => s.cookies.get(a), getAll: () => s.cookies.getAll() } }, p = { setHeader: (a, o) => { if (a === "Set-Cookie") { const u = Array.isArray(o) ? o : [o]; for (const n of u) { const [l, ...h] = n.split(";"), [d, i] = l.split("="); if (d && i) { const f = { httpOnly: e.cookieOptions.httpOnly, secure: e.cookieOptions.secure, sameSite: e.cookieOptions.sameSite, maxAge: e.cookieOptions.maxAge, path: "/" // Ensure path is set }; for (const x of h) { const [g, P] = x.trim().split("="); switch (g.toLowerCase()) { case "max-age": f.maxAge = Number.parseInt(P, 10); break; case "expires": f.expires = new Date(P); break; case "path": f.path = P; break; case "domain": f.domain = P; break; case "secure": f.secure = !0; break; case "httponly": f.httpOnly = !0; break; case "samesite": f.sameSite = P; break; } } t.cookies.set( d.trim(), i.trim(), f ); } } } else t.headers.set( a, Array.isArray(o) ? o.join(", ") : o ); }, getHeader: (a) => t.headers.get(a) }; return new R(k, p); } function T(s, t, e) { O(t, e, s); const r = b(s.storageKeyPrefix, { req: t, res: { setHeader: (p, a) => { if (p === "Set-Cookie") { const o = Array.isArray(a) ? a : [a]; for (const u of o) { const [n, ...l] = u.split(";"), [h, d] = n.split("="); if (h && d) { const i = { httpOnly: s.cookieOptions.httpOnly, secure: s.cookieOptions.secure, sameSite: s.cookieOptions.sameSite, maxAge: s.cookieOptions.maxAge }; for (const f of l) { const [x, g] = f.trim().split("="); switch (x.toLowerCase()) { case "max-age": i.maxAge = Number.parseInt(g, 10); break; case "expires": i.expires = new Date(g); break; case "path": i.path = g; break; case "domain": i.domain = g; break; case "secure": i.secure = !0; break; case "httponly": i.httpOnly = !0; break; case "samesite": i.sameSite = g; break; } } e.cookies.set( h.trim(), d.trim(), i ); } } } else e.headers.set( p, Array.isArray(a) ? a.join(", ") : a ); }, getHeader: (p) => e.headers.get(p) } }); return c(s, "Storage tokens:", { accessToken: r.getAccessToken() ? "[PRESENT]" : "[MISSING]", refreshToken: r.getRefreshToken() ? "[PRESENT]" : "[MISSING]", sessionId: r.getSessionId() ? "[PRESENT]" : "[MISSING]", remoteSessionCookie: t.cookies.get("frank_sid")?.value, storageKeyPrefix: s.storageKeyPrefix, userType: s.userType, projectId: s.projectId, secretKey: s.secretKey, apiUrl: s.apiUrl }), new I({ apiUrl: s.apiUrl, publishableKey: s.publishableKey, sessionCookieName: s.sessionCookieName, storageKeyPrefix: s.storageKeyPrefix, userType: s.userType, projectId: s.projectId, secretKey: s.secretKey, storage: r, debug: s.debug, debugConfig: { logLevel: "debug" } }); } async function y(s, t, e) { try { c(e, "Validating authentication using AuthSDK"); const r = !!t.authStorage.getAccessToken(), k = !!t.authStorage.getRefreshToken(), a = !!s.cookies.get(e.sessionCookieName)?.value; if (c(e, "Authentication state:", { hasAccessToken: r, hasRefreshToken: k, hasSessionCookie: a, sessionCookieName: e.sessionCookieName }), !r && !k && !a) return c(e, "No tokens or session cookies found, skipping API call"), { isAuthenticated: !1, user: null, session: null, organizationId: null, error: null, tokenInfo: { accessTokenExpired: !0, refreshTokenExpired: !0, canRefresh: !1 } }; const o = t.getTokenExpirationInfo(); if (c(e, "Token expiration info:", { accessToken: { isExpired: o.accessToken.isExpired, expiresIn: o.accessToken.expiresIn }, refreshToken: { isExpired: o.refreshToken.isExpired, expiresIn: o.refreshToken.expiresIn } }), e.skipApiCallOnNetworkError && process.env.NODE_ENV === "development" && (c( e, "Skipping API call due to development mode network configuration" ), r && !o.accessToken.isExpired || a)) return { isAuthenticated: !0, user: null, // We don't have user data without API call session: null, organizationId: e.projectId || null, error: null, tokenInfo: { accessTokenExpired: o.accessToken.isExpired, refreshTokenExpired: o.refreshToken.isExpired, canRefresh: !o.refreshToken.isExpired } }; const u = async () => { const d = new AbortController(), i = setTimeout(() => d.abort(), 5e3), f = s.cookies.getAll().map((x) => `${x.name}=${x.value}`).join("; "); try { const x = await t.getAuthStatus({ credentials: "include", signal: d.signal, headers: { "User-Agent": "FrankAuth-Middleware/1.0", Accept: "application/json", "Content-Type": "application/json", // Copy essential headers only "X-Forwarded-For": s.headers.get("x-forwarded-for") || "", "X-Real-IP": s.headers.get("x-real-ip") || "", Cookie: f }, // Add retry configuration cache: "no-cache", keepalive: !1 }); return clearTimeout(i), x; } catch (x) { throw clearTimeout(i), x; } }; let n, l = null; const h = e.maxRetries || 2; for (let d = 1; d <= h; d++) try { c(e, `Auth status attempt ${d}/${h}`), n = await u(); break; } catch (i) { if (l = i, c(e, `Auth status attempt ${d} failed:`, { name: i.name, message: i.message, code: i.code }), d === h) { if (e.fallbackToLocalTokens && (r && !o.accessToken.isExpired || a)) return c( e, "API failed but local token/session is valid, trusting local state" ), { isAuthenticated: !0, user: null, session: null, organizationId: e.projectId || null, error: i, tokenInfo: { accessTokenExpired: o.accessToken.isExpired, refreshTokenExpired: o.refreshToken.isExpired, canRefresh: !o.refreshToken.isExpired } }; throw i; } if (!N(i)) throw i; const f = Math.min(1e3 * Math.pow(2, d - 1), 5e3), x = Math.random() * 0.1 * f; await new Promise((g) => setTimeout(g, f + x)); } return c(e, "Auth status received:", { isAuthenticated: n.isAuthenticated, hasUser: !!n.user, organizationId: n.organizationId }), { isAuthenticated: n.isAuthenticated, user: n.user || null, session: n.session || null, organizationId: n.organizationId || null, error: null, tokenInfo: { accessTokenExpired: o.accessToken.isExpired, refreshTokenExpired: o.refreshToken.isExpired, canRefresh: !o.refreshToken.isExpired } }; } catch (r) { c(e, "Authentication validation error:", { name: r.name, message: r.message, code: r.code }); const k = t.getTokenExpirationInfo(); return { isAuthenticated: !1, user: null, session: null, organizationId: null, error: r, tokenInfo: { // Don't mark tokens as expired just because of network errors accessTokenExpired: k.accessToken.isExpired, refreshTokenExpired: k.refreshToken.isExpired, canRefresh: !k.refreshToken.isExpired && k.refreshToken.isValid } }; } } function N(s) { const t = [ "NETWORK_ERROR", "ECONNREFUSED", "ENOTFOUND", "ECONNRESET", "ETIMEDOUT", "AbortError" ]; return s?.code && t.includes(s.code) || s?.name === "FrankAuthNetworkError" || s?.message?.includes("fetch failed") || s?.message?.includes("network") || s?.name === "AbortError"; } function C(s, t) { if (!t.enableOrgRouting) return null; const e = s.nextUrl.hostname; if (t.customDomain && e === t.customDomain) return s.nextUrl.searchParams.get("org") || null; const r = e.split("."); return r.length > 2 ? r[0] : null; } function c(s, t, e) { s.debug && console.log(`[FrankAuth Middleware] ${t}`, e || ""); } async function U(s, t) { const e = s.nextUrl.pathname; if (c(t, `Processing request: ${e}`), A(e, t.ignorePaths)) return c(t, `Ignoring path: ${e}`), m.next(); const r = m.next(); if (t.hooks?.beforeAuth) { const h = await t.hooks.beforeAuth(s); if (h instanceof m) return h; h instanceof S && (s = h); } const k = T(t, s, r), p = A(e, t.publicPaths), a = A(e, t.authPaths); let o; t.allPathsPrivate ? o = !p && !a : o = A(e, t.privatePaths), c(t, "Path analysis:", { isPublicPath: p, isPrivatePath: o, isAuthPath: a, allPathsPrivate: t.allPathsPrivate }); const u = await y(s, k, t); c(t, "Authentication result:", { isAuthenticated: u.isAuthenticated, hasUser: !!u.user, organizationId: u.organizationId, tokenInfo: u.tokenInfo }); const l = await v({ req: s, config: t, auth: u, path: e, isPublicPath: p, isPrivatePath: o, isAuthPath: a, response: r }); if (t.hooks?.afterAuth) { const h = await t.hooks.afterAuth(s, l, u); if (h instanceof m) return h; } return l; } async function v(s) { const { req: t, config: e, auth: r, path: k, isPublicPath: p, isPrivatePath: a, isAuthPath: o, response: u } = s; try { if (r.isAuthenticated && r.user) { if (c(e, "User is authenticated"), e.hooks?.onAuthenticated && r.session) { const n = await e.hooks.onAuthenticated( t, r.user, r.session ); if (n instanceof m) return n; } if (o) { const n = t.nextUrl.searchParams.get("redirect_url") || e.afterSignInPath; c( e, `Redirecting authenticated user from auth path to: ${n}` ); const l = m.redirect( new URL(n, t.url) ); return E(u, l), l; } if (e.enableOrgRouting && !r.organizationId && k !== e.orgSelectionPath) { if (c(e, "Organization required but not selected"), e.hooks?.onOrganizationRequired) { const l = await e.hooks.onOrganizationRequired( t, r.user ); if (l instanceof m) return l; } const n = m.redirect( new URL(e.orgSelectionPath, t.url) ); return E(u, n), n; } return u; } if (c(e, "User is not authenticated"), e.hooks?.onUnauthenticated) { const n = await e.hooks.onUnauthenticated(t); if (n instanceof m) return n; } if (p || o) return c(e, "Allowing access to public/auth path"), u; if (a) { const n = new URL(e.signInPath, t.url); n.searchParams.set( "redirect_url", t.nextUrl.pathname + t.nextUrl.search ), c(e, `Redirecting to sign in: ${n.toString()}`); const l = m.redirect(n); return E(u, l), l; } return u; } catch (n) { if (c(e, "Error in authentication handling:", n), e.hooks?.onError) { const d = await e.hooks.onError(t, n); if (d instanceof m) return d; } const l = new URL(e.signInPath, t.url); l.searchParams.set("error", "auth_error"); const h = m.redirect(l); return E(u, h), h; } } function E(s, t) { try { const e = s.headers.getSetCookie(); if (e.length > 0) for (const r of e) t.headers.append("Set-Cookie", r); for (const r of s.cookies.getAll()) r.name && r.value && t.cookies.set(r.name, r.value, { domain: r.domain, expires: r.expires, httpOnly: r.httpOnly, maxAge: r.maxAge, path: r.path || "/", // Ensure path is always set secure: r.secure, sameSite: r.sameSite }); } catch (e) { console.error("Error copying response cookies:", e); } } function D(s) { const t = { ...w, ...s }; if (!t.publishableKey) throw new Error("publishableKey is required for Frank Auth middleware"); return t.storageKeyPrefix || (t.storageKeyPrefix = "frank_auth"), c(t, "Frank Auth middleware initialized with config:", { publicPaths: t.publicPaths, privatePaths: t.privatePaths, authPaths: t.authPaths, allPathsPrivate: t.allPathsPrivate, signInPath: t.signInPath, enableOrgRouting: t.enableOrgRouting, storageKeyPrefix: t.storageKeyPrefix }), async function(r) { return U(r, t); }; } function _(s) { return function(e) { return s.custom ? s.custom(e) : s.exclude && A(e, s.exclude) ? !1 : !!(s.include && A(e, s.include)); }; } async function j(s, t, e) { try { const r = m.next(), k = T( e, s, r ), p = s.cookies.getAll().map((o) => `${o.name}=${o.value}`).join("; "); return (await k.getAuthStatus({ headers: { Cookie: p } })).isAuthenticated; } catch { return !1; } } function F(s, t) { return C(s, t); } function M(s, t) { const e = m.next(); return T(t, s, e); } async function L(s, t) { const e = m.next(), r = T( t, s, e ); return y( s, r, t ); } export { L as checkAuthStatus, j as checkPermission, D as createFrankAuthMiddleware, _ as createMatcher, M as getAuthSDKFromRequest, F as getOrganizationFromRequest }; //# sourceMappingURL=index.js.map