This commit is contained in:
@@ -6,91 +6,91 @@ const DEFAULT_REFRESH_TOKEN_KEY = 'refreshToken';
|
||||
const DEFAULT_LEGACY_KEYS = ['auth_token', 'auth_user', 'token'];
|
||||
|
||||
export type AuthState<TUser> = {
|
||||
authToken: string | null;
|
||||
refreshToken: string | null;
|
||||
currentUser: TUser | null;
|
||||
authToken: string | null;
|
||||
refreshToken: string | null;
|
||||
currentUser: TUser | null;
|
||||
};
|
||||
|
||||
export type AuthContextValue<TUser> = AuthState<TUser> & {
|
||||
setSession: (authToken: string, refreshToken: string, currentUser: TUser) => void;
|
||||
setCurrentUser: (currentUser: TUser | null) => void;
|
||||
clearSession: () => void;
|
||||
setSession: (authToken: string, refreshToken: string, currentUser: TUser) => void;
|
||||
setCurrentUser: (currentUser: TUser | null) => void;
|
||||
clearSession: () => void;
|
||||
};
|
||||
|
||||
export type CreateAuthContextOptions = {
|
||||
authTokenKey?: string;
|
||||
refreshTokenKey?: string;
|
||||
legacyKeys?: string[];
|
||||
authTokenKey?: string;
|
||||
refreshTokenKey?: string;
|
||||
legacyKeys?: string[];
|
||||
};
|
||||
|
||||
export function createAuthContext<TUser>(options: CreateAuthContextOptions = {}) {
|
||||
const authTokenKey = options.authTokenKey ?? DEFAULT_AUTH_TOKEN_KEY;
|
||||
const refreshTokenKey = options.refreshTokenKey ?? DEFAULT_REFRESH_TOKEN_KEY;
|
||||
const legacyKeys = options.legacyKeys ?? DEFAULT_LEGACY_KEYS;
|
||||
const authTokenKey = options.authTokenKey ?? DEFAULT_AUTH_TOKEN_KEY;
|
||||
const refreshTokenKey = options.refreshTokenKey ?? DEFAULT_REFRESH_TOKEN_KEY;
|
||||
const legacyKeys = options.legacyKeys ?? DEFAULT_LEGACY_KEYS;
|
||||
|
||||
const AuthContext = createContext<AuthContextValue<TUser> | undefined>(undefined);
|
||||
const AuthContext = createContext<AuthContextValue<TUser> | undefined>(undefined);
|
||||
|
||||
function readStoredSession(): AuthState<TUser> {
|
||||
for (const key of legacyKeys) {
|
||||
localStorage.removeItem(key);
|
||||
function readStoredSession(): AuthState<TUser> {
|
||||
for (const key of legacyKeys) {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
|
||||
const authToken = localStorage.getItem(authTokenKey);
|
||||
const refreshToken = localStorage.getItem(refreshTokenKey);
|
||||
|
||||
return {
|
||||
authToken,
|
||||
refreshToken,
|
||||
currentUser: null,
|
||||
};
|
||||
}
|
||||
|
||||
const authToken = localStorage.getItem(authTokenKey);
|
||||
const refreshToken = localStorage.getItem(refreshTokenKey);
|
||||
function AuthProvider({ children }: Readonly<{ children: ReactNode }>) {
|
||||
const [state, setState] = useState<AuthState<TUser>>(readStoredSession);
|
||||
|
||||
const setSession = useCallback(
|
||||
(authToken: string, refreshToken: string, currentUser: TUser) => {
|
||||
localStorage.setItem(authTokenKey, authToken);
|
||||
localStorage.setItem(refreshTokenKey, refreshToken);
|
||||
setState({ authToken, refreshToken, currentUser });
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const clearSession = useCallback(() => {
|
||||
localStorage.removeItem(authTokenKey);
|
||||
localStorage.removeItem(refreshTokenKey);
|
||||
setState({ authToken: null, refreshToken: null, currentUser: null });
|
||||
}, []);
|
||||
|
||||
const setCurrentUser = useCallback((currentUser: TUser | null) => {
|
||||
setState((prev) => ({ ...prev, currentUser }));
|
||||
}, []);
|
||||
|
||||
const value = useMemo<AuthContextValue<TUser>>(
|
||||
() => ({
|
||||
...state,
|
||||
setSession,
|
||||
setCurrentUser,
|
||||
clearSession,
|
||||
}),
|
||||
[state, setSession, setCurrentUser, clearSession],
|
||||
);
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
}
|
||||
|
||||
function useAuth() {
|
||||
const ctx = useContext(AuthContext);
|
||||
if (!ctx) {
|
||||
throw new Error('useAuth must be used within AuthProvider');
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
|
||||
return {
|
||||
authToken,
|
||||
refreshToken,
|
||||
currentUser: null,
|
||||
AuthProvider,
|
||||
useAuth,
|
||||
AuthContext,
|
||||
};
|
||||
}
|
||||
|
||||
function AuthProvider({ children }: Readonly<{ children: ReactNode }>) {
|
||||
const [state, setState] = useState<AuthState<TUser>>(readStoredSession);
|
||||
|
||||
const setSession = useCallback(
|
||||
(authToken: string, refreshToken: string, currentUser: TUser) => {
|
||||
localStorage.setItem(authTokenKey, authToken);
|
||||
localStorage.setItem(refreshTokenKey, refreshToken);
|
||||
setState({ authToken, refreshToken, currentUser });
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const clearSession = useCallback(() => {
|
||||
localStorage.removeItem(authTokenKey);
|
||||
localStorage.removeItem(refreshTokenKey);
|
||||
setState({ authToken: null, refreshToken: null, currentUser: null });
|
||||
}, []);
|
||||
|
||||
const setCurrentUser = useCallback((currentUser: TUser | null) => {
|
||||
setState((prev) => ({ ...prev, currentUser }));
|
||||
}, []);
|
||||
|
||||
const value = useMemo<AuthContextValue<TUser>>(
|
||||
() => ({
|
||||
...state,
|
||||
setSession,
|
||||
setCurrentUser,
|
||||
clearSession,
|
||||
}),
|
||||
[state, setSession, setCurrentUser, clearSession],
|
||||
);
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
}
|
||||
|
||||
function useAuth() {
|
||||
const ctx = useContext(AuthContext);
|
||||
if (!ctx) {
|
||||
throw new Error('useAuth must be used within AuthProvider');
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
|
||||
return {
|
||||
AuthProvider,
|
||||
useAuth,
|
||||
AuthContext,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,35 +1,35 @@
|
||||
const MILLISECONDS_PER_SECOND = 1000;
|
||||
|
||||
export function decodeJwtPayload(token: string): Record<string, unknown> | null {
|
||||
const parts = token.split('.');
|
||||
if (parts.length !== 3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const base64Url = parts[1];
|
||||
const base64 = base64Url.replaceAll('-', '+').replaceAll('_', '/');
|
||||
const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), '=');
|
||||
|
||||
try {
|
||||
const payload = JSON.parse(atob(padded));
|
||||
if (payload && typeof payload === 'object') {
|
||||
return payload as Record<string, unknown>;
|
||||
const parts = token.split('.');
|
||||
if (parts.length !== 3) {
|
||||
return null;
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
const base64Url = parts[1];
|
||||
const base64 = base64Url.replaceAll('-', '+').replaceAll('_', '/');
|
||||
const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), '=');
|
||||
|
||||
try {
|
||||
const payload = JSON.parse(atob(padded));
|
||||
if (payload && typeof payload === 'object') {
|
||||
return payload as Record<string, unknown>;
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function isJwtExpired(token: string, skewSeconds = 0) {
|
||||
const payload = decodeJwtPayload(token);
|
||||
const exp = payload?.exp;
|
||||
if (typeof exp !== 'number' || !Number.isFinite(exp)) {
|
||||
return false;
|
||||
}
|
||||
const payload = decodeJwtPayload(token);
|
||||
const exp = payload?.exp;
|
||||
if (typeof exp !== 'number' || !Number.isFinite(exp)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const expiresAt = exp * MILLISECONDS_PER_SECOND;
|
||||
const now = Date.now();
|
||||
return expiresAt <= now + skewSeconds * MILLISECONDS_PER_SECOND;
|
||||
const expiresAt = exp * MILLISECONDS_PER_SECOND;
|
||||
const now = Date.now();
|
||||
return expiresAt <= now + skewSeconds * MILLISECONDS_PER_SECOND;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user