From cf8781cc11249c79cf97b1dffe127702d02286a3 Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Thu, 12 Feb 2026 11:22:41 -0800 Subject: [PATCH] feat(react-auth): add @bytelyst/react-auth package - createAuthProvider() factory returns typed AuthProvider + useAuth hook - Configurable storagePrefix, loginEndpoint, mapLoginResponse, onLogout - localStorage persistence for user, access token, refresh token - Uses @bytelyst/api-client for login requests - Replaces 3 duplicated auth-context.tsx files across LysnrAI dashboards - Peer dep: react >=18.0.0, workspace dep: @bytelyst/api-client --- packages/react-auth/package.json | 24 +++++ packages/react-auth/src/auth-context.tsx | 109 +++++++++++++++++++++++ packages/react-auth/src/index.ts | 2 + packages/react-auth/src/types.ts | 27 ++++++ packages/react-auth/tsconfig.json | 11 +++ 5 files changed, 173 insertions(+) create mode 100644 packages/react-auth/package.json create mode 100644 packages/react-auth/src/auth-context.tsx create mode 100644 packages/react-auth/src/index.ts create mode 100644 packages/react-auth/src/types.ts create mode 100644 packages/react-auth/tsconfig.json diff --git a/packages/react-auth/package.json b/packages/react-auth/package.json new file mode 100644 index 00000000..86302f59 --- /dev/null +++ b/packages/react-auth/package.json @@ -0,0 +1,24 @@ +{ + "name": "@bytelyst/react-auth", + "version": "0.1.0", + "type": "module", + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": ["dist"], + "scripts": { + "build": "tsc", + "test": "vitest run" + }, + "peerDependencies": { + "react": ">=18.0.0" + }, + "dependencies": { + "@bytelyst/api-client": "workspace:*" + } +} diff --git a/packages/react-auth/src/auth-context.tsx b/packages/react-auth/src/auth-context.tsx new file mode 100644 index 00000000..f72cfefe --- /dev/null +++ b/packages/react-auth/src/auth-context.tsx @@ -0,0 +1,109 @@ +"use client"; + +import { + createContext, + useContext, + useState, + useCallback, + type ReactNode, +} from "react"; +import { createApiClient } from "@bytelyst/api-client"; +import type { AuthConfig, AuthContextValue, BaseUser } from "./types.js"; + +/** + * Create a typed auth provider + hook for a specific user type. + * + * @example + * ```tsx + * const { AuthProvider, useAuth } = createAuthProvider({ + * storagePrefix: "admin", + * loginEndpoint: "/auth/login", + * mapLoginResponse: (data) => ({ + * user: data.user, + * accessToken: data.accessToken, + * refreshToken: data.refreshToken, + * }), + * }); + * ``` + */ +export function createAuthProvider( + config: AuthConfig, +) { + const { storagePrefix, loginEndpoint, mapLoginResponse, onLogout } = config; + + const USER_KEY = `${storagePrefix}_auth_user`; + const TOKEN_KEY = `${storagePrefix}_access_token`; + const REFRESH_KEY = `${storagePrefix}_refresh_token`; + + const AuthContext = createContext | null>(null); + + function getStoredUser(): TUser | null { + if (typeof window === "undefined") return null; + try { + const stored = localStorage.getItem(USER_KEY); + return stored ? JSON.parse(stored) : null; + } catch { + return null; + } + } + + function AuthProvider({ children }: { children: ReactNode }) { + const [user, setUser] = useState(getStoredUser); + const isLoading = false; + + const api = createApiClient({ + baseUrl: "/api", + getToken: () => + typeof window !== "undefined" + ? localStorage.getItem(TOKEN_KEY) + : null, + }); + + const login = useCallback( + async (email: string, password: string) => { + const { data, error } = await api.safeFetch(loginEndpoint, { + method: "POST", + body: JSON.stringify({ email, password }), + }); + + if (data && !error) { + const mapped = mapLoginResponse(data); + setUser(mapped.user); + localStorage.setItem(USER_KEY, JSON.stringify(mapped.user)); + localStorage.setItem(TOKEN_KEY, mapped.accessToken); + localStorage.setItem(REFRESH_KEY, mapped.refreshToken); + return true; + } + + return false; + }, + [api], + ); + + const logout = useCallback(() => { + setUser(null); + localStorage.removeItem(USER_KEY); + localStorage.removeItem(TOKEN_KEY); + localStorage.removeItem(REFRESH_KEY); + onLogout?.(); + }, []); + + return ( + + {children} + + ); + } + + function useAuth(): AuthContextValue { + const ctx = useContext(AuthContext); + if (!ctx) { + throw new Error("useAuth must be used within an AuthProvider"); + } + return ctx; + } + + return { AuthProvider, useAuth }; +} diff --git a/packages/react-auth/src/index.ts b/packages/react-auth/src/index.ts new file mode 100644 index 00000000..d4400a53 --- /dev/null +++ b/packages/react-auth/src/index.ts @@ -0,0 +1,2 @@ +export { createAuthProvider } from "./auth-context.js"; +export type { BaseUser, AuthContextValue, AuthConfig } from "./types.js"; diff --git a/packages/react-auth/src/types.ts b/packages/react-auth/src/types.ts new file mode 100644 index 00000000..bfe1eff0 --- /dev/null +++ b/packages/react-auth/src/types.ts @@ -0,0 +1,27 @@ +import type { ReactNode } from "react"; + +export interface BaseUser { + email: string; + name: string; + role: string; + [key: string]: unknown; +} + +export interface AuthContextValue { + user: TUser | null; + isAuthenticated: boolean; + isLoading: boolean; + login: (email: string, password: string) => Promise; + logout: () => void; +} + +export interface AuthConfig { + storagePrefix: string; + loginEndpoint: string; + mapLoginResponse: (data: unknown) => { + user: TUser; + accessToken: string; + refreshToken: string; + }; + onLogout?: () => void; +} diff --git a/packages/react-auth/tsconfig.json b/packages/react-auth/tsconfig.json new file mode 100644 index 00000000..4447784f --- /dev/null +++ b/packages/react-auth/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "lib": ["ES2022", "DOM"], + "jsx": "react-jsx" + }, + "include": ["src"], + "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"] +}