@deskree/atomic-auth-sdk
Version:
Unified Ory Kratos authentication SDK for frontend projects
587 lines (574 loc) • 16.3 kB
JavaScript
// 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
};