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:
parent
01624a2231
commit
cf8781cc11
24
packages/react-auth/package.json
Normal file
24
packages/react-auth/package.json
Normal 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:*"
|
||||
}
|
||||
}
|
||||
109
packages/react-auth/src/auth-context.tsx
Normal file
109
packages/react-auth/src/auth-context.tsx
Normal 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 };
|
||||
}
|
||||
2
packages/react-auth/src/index.ts
Normal file
2
packages/react-auth/src/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { createAuthProvider } from "./auth-context.js";
|
||||
export type { BaseUser, AuthContextValue, AuthConfig } from "./types.js";
|
||||
27
packages/react-auth/src/types.ts
Normal file
27
packages/react-auth/src/types.ts
Normal 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;
|
||||
}
|
||||
11
packages/react-auth/tsconfig.json
Normal file
11
packages/react-auth/tsconfig.json
Normal 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"]
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user