@frank-auth/react
Version:
Flexible and customizable React UI components for Frank Authentication
580 lines (579 loc) • 16.6 kB
JavaScript
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