feat(react-auth): add @bytelyst/react-auth package

- createAuthProvider<TUser>() 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
This commit is contained in:
saravanakumardb1 2026-02-12 11:22:41 -08:00
parent 01624a2231
commit cf8781cc11
5 changed files with 173 additions and 0 deletions

View File

@ -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:*"
}
}

View File

@ -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<AdminUser>({
* storagePrefix: "admin",
* loginEndpoint: "/auth/login",
* mapLoginResponse: (data) => ({
* user: data.user,
* accessToken: data.accessToken,
* refreshToken: data.refreshToken,
* }),
* });
* ```
*/
export function createAuthProvider<TUser extends BaseUser = BaseUser>(
config: AuthConfig<TUser>,
) {
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<AuthContextValue<TUser> | 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<TUser | null>(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<unknown>(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 (
<AuthContext.Provider
value={{ user, isAuthenticated: !!user, isLoading, login, logout }}
>
{children}
</AuthContext.Provider>
);
}
function useAuth(): AuthContextValue<TUser> {
const ctx = useContext(AuthContext);
if (!ctx) {
throw new Error("useAuth must be used within an AuthProvider");
}
return ctx;
}
return { AuthProvider, useAuth };
}

View File

@ -0,0 +1,2 @@
export { createAuthProvider } from "./auth-context.js";
export type { BaseUser, AuthContextValue, AuthConfig } from "./types.js";

View File

@ -0,0 +1,27 @@
import type { ReactNode } from "react";
export interface BaseUser {
email: string;
name: string;
role: string;
[key: string]: unknown;
}
export interface AuthContextValue<TUser extends BaseUser = BaseUser> {
user: TUser | null;
isAuthenticated: boolean;
isLoading: boolean;
login: (email: string, password: string) => Promise<boolean>;
logout: () => void;
}
export interface AuthConfig<TUser extends BaseUser = BaseUser> {
storagePrefix: string;
loginEndpoint: string;
mapLoginResponse: (data: unknown) => {
user: TUser;
accessToken: string;
refreshToken: string;
};
onLogout?: () => void;
}

View File

@ -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"]
}