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