UNPKG

@deskree/atomic-auth-sdk

Version:

Unified Ory Kratos authentication SDK for frontend projects

587 lines (574 loc) 16.3 kB
// src/client/index.ts import { Configuration, FrontendApi } from "@ory/client"; var OryAuthClient = class { constructor(config) { var _a; this.basePath = config.basePath; this.client = new FrontendApi( new Configuration({ basePath: config.basePath, baseOptions: { withCredentials: (_a = config.withCredentials) != null ? _a : true } }) ); } async getSession() { try { const { data } = await this.client.toSession(); return { session: data, error: null }; } catch (error) { return { session: null, error }; } } async refreshSession() { try { const { data } = await this.client.toSession(); return { session: data, error: null }; } catch (error) { return { session: null, error }; } } async logout() { try { const { data } = await this.client.createBrowserLogoutFlow(); return { logoutUrl: data.logout_url, error: null }; } catch (error) { return { logoutUrl: null, error }; } } async login(returnTo) { try { const { data } = await this.client.createBrowserLoginFlow({ returnTo }); return { loginUrl: data.request_url, error: null }; } catch (error) { return { loginUrl: null, error }; } } async register(returnTo) { try { const { data } = await this.client.createBrowserRegistrationFlow({ returnTo }); return { registrationUrl: data.request_url, error: null }; } catch (error) { return { registrationUrl: null, error }; } } // Create a verification flow for email/phone verification async createVerificationFlow(returnTo) { try { const { data } = await this.client.createBrowserVerificationFlow({ returnTo }); return { verificationUrl: data.request_url, error: null, flowId: data.id }; } catch (error) { return { verificationUrl: null, error, flowId: null }; } } // Send verification email with code method async sendVerificationCode(email, flowId) { var _a, _b; try { const { data: flow } = await this.client.getVerificationFlow({ id: flowId }); const csrfToken = ((_a = flow.ui.nodes.find( (node) => { var _a2; return ((_a2 = node.attributes) == null ? void 0 : _a2.name) === "csrf_token"; } )) == null ? void 0 : _a.attributes) ? ((_b = flow.ui.nodes.find( (node) => { var _a2; return ((_a2 = node.attributes) == null ? void 0 : _a2.name) === "csrf_token"; } )) == null ? void 0 : _b.attributes).value : ""; const { data } = await this.client.updateVerificationFlow({ flow: flowId, updateVerificationFlowBody: { method: "code", email, csrf_token: csrfToken } }); return { success: true, error: null, flow: data }; } catch (error) { return { success: false, error, flow: null }; } } // Verify code for email verification async verifyCode(flowId, code) { var _a, _b; try { const { data: flow } = await this.client.getVerificationFlow({ id: flowId }); const csrfToken = ((_a = flow.ui.nodes.find( (node) => { var _a2; return ((_a2 = node.attributes) == null ? void 0 : _a2.name) === "csrf_token"; } )) == null ? void 0 : _a.attributes) ? ((_b = flow.ui.nodes.find( (node) => { var _a2; return ((_a2 = node.attributes) == null ? void 0 : _a2.name) === "csrf_token"; } )) == null ? void 0 : _b.attributes).value : ""; const { data } = await this.client.updateVerificationFlow({ flow: flowId, updateVerificationFlowBody: { method: "code", code, csrf_token: csrfToken } }); const isVerified = data.state === "passed_challenge"; return { success: isVerified, error: null, flow: data }; } catch (error) { return { success: false, error, flow: null }; } } // Get verification flow status async getVerificationFlow(flowId) { try { const { data } = await this.client.getVerificationFlow({ id: flowId }); return { flow: data, error: null }; } catch (error) { return { flow: null, error }; } } // Check if identity is verified based on the session isIdentityVerified(session) { if (!session || !session.identity) return false; const identity = session.identity; const verifiedAddresses = identity.verifiable_addresses || []; return verifiedAddresses.length > 0; } }; // src/context/ClientAuthProvider.tsx import React2 from "react"; // src/context/AuthProvider.tsx import React, { useState, useEffect } from "react"; // src/session/validation.ts function extractUserFromSession(session) { var _a, _b; const identity = session.identity; return { id: identity.id, email: identity.traits.email, name: { first: (_a = identity.traits.name) == null ? void 0 : _a.first, last: (_b = identity.traits.name) == null ? void 0 : _b.last } }; } function isSessionValid(session) { if (!session) return false; if (session.expires_at) { const expiresAt = new Date(session.expires_at); if (expiresAt < /* @__PURE__ */ new Date()) return false; } return true; } // src/session/storage.ts var isBrowser = typeof window !== "undefined"; function saveUserToStorage(userData, key) { if (!isBrowser) return; try { localStorage.setItem(key, JSON.stringify(userData)); } catch (error) { console.error("Failed to save user data to localStorage:", error); } } function getUserFromStorage(key) { if (!isBrowser) return null; try { const data = localStorage.getItem(key); return data ? JSON.parse(data) : null; } catch (error) { console.error("Failed to parse user data from localStorage:", error); return null; } } function removeUserFromStorage(key) { if (!isBrowser) return; localStorage.removeItem(key); } // src/context/auth-context.ts import { createContext } from "react"; var AuthContext = createContext({ isLoading: true, isAuthenticated: false, user: null, session: null, error: null, isVerified: false, login: async () => { }, logout: async () => { }, refresh: async () => { }, refreshSession: async () => false, startVerification: async () => ({ verificationUrl: null, error: null, flowId: null }), sendVerificationCode: async () => ({ success: false, error: null, flow: null }), verifyCode: async () => ({ success: false, error: null, flow: null }), getVerificationFlow: async () => ({ flow: null, error: null }), checkVerification: async () => false }); // src/context/AuthProvider.tsx function AuthProvider({ children, client, loginPath = "/login", storageKey = "user_data", basePath = process.env.NEXT_PUBLIC_ORY_SDK_URL || "http://localhost:4455" }) { const [authClient] = useState( () => client || new OryAuthClient({ basePath }) ); const [isLoading, setIsLoading] = useState(true); const [session, setSession] = useState(null); const [user, setUser] = useState(null); const [error, setError] = useState(null); const [isVerified, setIsVerified] = useState(false); const extractUserFromSession2 = (session2) => { var _a, _b; const identity = session2.identity; const verifiedAddresses = identity.verifiable_addresses || []; const isVerified2 = verifiedAddresses.length > 0; return { id: identity.id, email: identity.traits.email, verified: isVerified2, name: { first: (_a = identity.traits.name) == null ? void 0 : _a.first, last: (_b = identity.traits.name) == null ? void 0 : _b.last } }; }; const refresh = async () => { try { setIsLoading(true); const { session: newSession, error: error2 } = await authClient.getSession(); if (error2) { throw error2; } if (newSession && isSessionValid(newSession)) { setSession(newSession); const userData = extractUserFromSession2(newSession); setUser(userData); saveUserToStorage(userData, storageKey); } else { setSession(null); setUser(null); removeUserFromStorage(storageKey); } setError(null); } catch (err) { setError( err instanceof Error ? err : new Error("Failed to refresh session") ); setSession(null); setUser(null); removeUserFromStorage(storageKey); } finally { setIsLoading(false); } }; const refreshSession = async () => { try { setIsLoading(true); const { session: newSession, error: error2 } = await authClient.refreshSession(); if (error2) { throw error2; } if (newSession) { setSession(newSession); const userData = extractUserFromSession2(newSession); setUser(userData); setIsVerified(userData.verified); saveUserToStorage(userData, storageKey); return true; } else { setSession(null); setUser(null); setIsVerified(false); removeUserFromStorage(storageKey); return false; } setError(null); } catch (err) { setError( err instanceof Error ? err : new Error("Failed to refresh session") ); setSession(null); setUser(null); setIsVerified(false); removeUserFromStorage(storageKey); return false; } finally { setIsLoading(false); } }; const login = async (returnTo) => { const { loginUrl, error: error2 } = await authClient.login(returnTo); if (error2) { setError( error2 instanceof Error ? error2 : new Error("Failed to initiate login") ); return; } if (loginUrl) { window.location.href = loginUrl; } else { window.location.href = loginPath; } }; const logout = async () => { try { const { logoutUrl, error: error2 } = await authClient.logout(); if (error2) throw error2; removeUserFromStorage(storageKey); setUser(null); setSession(null); if (logoutUrl) { window.location.href = logoutUrl; } } catch (err) { setError(err instanceof Error ? err : new Error("Failed to logout")); } }; const startVerification = async (options = {}) => { const result = await authClient.createVerificationFlow(options.returnTo); return { verificationUrl: result.verificationUrl || null, error: result.error ? result.error : null, flowId: result.flowId || null }; }; const sendVerificationCode = async (email, flowId) => { const result = await authClient.sendVerificationCode(email, flowId); return { success: result.success, error: result.error ? result.error : null, flow: result.flow }; }; const verifyCode = async (flowId, code) => { const result = await authClient.verifyCode(flowId, code); if (result.success) { await refreshSession(); } return { success: result.success, error: result.error ? result.error : null, flow: result.flow }; }; const getVerificationFlow = async (flowId) => { const result = await authClient.getVerificationFlow(flowId); return { flow: result.flow, error: result.error ? result.error : null }; }; const checkVerification = async () => { try { await refreshSession(); return isVerified; } catch (err) { return false; } }; useEffect(() => { const savedUser = getUserFromStorage(storageKey); if (savedUser) { setUser(savedUser); setIsVerified(savedUser.verified || false); } refreshSession(); }, []); useEffect(() => { if (!session) return; const refreshTimer = setInterval(() => { refreshSession(); }, 5 * 60 * 1e3); return () => clearInterval(refreshTimer); }, [session]); const value = { isLoading, isAuthenticated: !!session, user, session, isVerified, error, login, logout, refreshSession, startVerification, sendVerificationCode, verifyCode, getVerificationFlow, checkVerification, refresh }; return /* @__PURE__ */ React.createElement(AuthContext.Provider, { value }, children); } // src/context/ClientAuthProvider.tsx function AuthProvider2({ children, basePath = process.env.NEXT_PUBLIC_ORY_SDK_URL || "http://localhost:4433" }) { const client = new OryAuthClient({ basePath, withCredentials: true }); return /* @__PURE__ */ React2.createElement(AuthProvider, { client }, children); } // src/hooks/useAuth.ts import { useContext } from "react"; function useAuth() { const context = useContext(AuthContext); if (context === void 0) { throw new Error("useAuth must be used within an AuthProvider"); } return context; } // src/createOryAuth.tsx import React4 from "react"; // src/middleware/withAuth.tsx import React3, { useEffect as useEffect2 } from "react"; import { useRouter } from "next/router"; function withAuth(Component, options = {}) { const WithAuthComponent = (props) => { const router = useRouter(); const { isAuthenticated, isLoading } = useAuth(); const loginPath = options.loginPath || "/login"; useEffect2(() => { if (!isLoading && !isAuthenticated) { const returnPath = encodeURIComponent(router.asPath); router.replace(`${loginPath}?returnTo=${returnPath}`); } }, [isLoading, isAuthenticated, router, loginPath]); if (isLoading) { return options.loadingComponent || /* @__PURE__ */ React3.createElement("div", null, "Loading..."); } if (!isAuthenticated) { return null; } return /* @__PURE__ */ React3.createElement(Component, { ...props }); }; WithAuthComponent.displayName = `withAuth(${Component.displayName || Component.name || "Component"})`; return WithAuthComponent; } // src/middleware/createAuthApi.ts function createAuthApi(handler, options) { return async (req, res) => { try { const { session, error } = await options.client.getSession(); if (error || !session || !isSessionValid(session)) { return res.status(401).json({ error: "Unauthorized", message: "You must be logged in to access this resource" }); } const user = extractUserFromSession(session); return handler(req, res, user); } catch (error) { console.error("Auth middleware error:", error); return res.status(500).json({ error: "Internal server error", message: "An error occurred while processing your request" }); } }; } // src/createOryAuth.tsx function createOryAuth(config) { const client = new OryAuthClient({ basePath: config.oryUrl, withCredentials: true }); function AuthProvider3({ children }) { return /* @__PURE__ */ React4.createElement(AuthProvider, { client }, children); } const refreshSession = async () => { const { session, error } = await client.refreshSession(); return { session, error }; }; const sendVerificationCode = async (email, flowId) => { return await client.sendVerificationCode(email, flowId); }; const verifyCode = async (flowId, code) => { return await client.verifyCode(flowId, code); }; const getVerificationFlow = async (flowId) => { return await client.getVerificationFlow(flowId); }; return { AuthProvider: AuthProvider3, useAuth, refreshSession, sendVerificationCode, verifyCode, getVerificationFlow, withAuth: (Component, options = {}) => withAuth(Component, { client, loginPath: config.loginPath, ...options }), createAuthApi: (handler) => createAuthApi(handler, { client }) }; } export { AuthProvider2 as AuthProvider, OryAuthClient, createOryAuth, useAuth };