mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2025-12-20 12:19:06 +00:00
session auto refresh blink
This commit is contained in:
@@ -8,9 +8,10 @@ export default function UserInfo() {
|
|||||||
const status = auth.status;
|
const status = auth.status;
|
||||||
const isLoading = status === "loading";
|
const isLoading = status === "loading";
|
||||||
const isAuthenticated = status === "authenticated";
|
const isAuthenticated = status === "authenticated";
|
||||||
|
const isRefreshing = status === "refreshing";
|
||||||
return isLoading ? (
|
return isLoading ? (
|
||||||
<Spinner size="xs" className="mx-3" />
|
<Spinner size="xs" className="mx-3" />
|
||||||
) : !isAuthenticated ? (
|
) : !isAuthenticated && !isRefreshing ? (
|
||||||
<Link
|
<Link
|
||||||
href="/"
|
href="/"
|
||||||
className="font-light px-2"
|
className="font-light px-2"
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { SessionAutoRefresh } from "./SessionAutoRefresh";
|
|||||||
|
|
||||||
type AuthContextType = (
|
type AuthContextType = (
|
||||||
| { status: "loading" }
|
| { status: "loading" }
|
||||||
|
| { status: "refreshing" }
|
||||||
| { status: "unauthenticated"; error?: string }
|
| { status: "unauthenticated"; error?: string }
|
||||||
| {
|
| {
|
||||||
status: "authenticated";
|
status: "authenticated";
|
||||||
@@ -30,23 +31,25 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
const customSession = session ? assertExtendedTokenAndUserId(session) : null;
|
const customSession = session ? assertExtendedTokenAndUserId(session) : null;
|
||||||
|
|
||||||
const contextValue: AuthContextType = {
|
const contextValue: AuthContextType = {
|
||||||
...(status === "loading"
|
...(status === "loading" && !customSession
|
||||||
? { status }
|
? { status }
|
||||||
: status === "authenticated" && customSession?.accessToken
|
: status === "loading" && customSession
|
||||||
? {
|
? { status: "refreshing" as const }
|
||||||
status,
|
: status === "authenticated" && customSession?.accessToken
|
||||||
accessToken: customSession.accessToken,
|
? {
|
||||||
accessTokenExpires: customSession.accessTokenExpires,
|
status,
|
||||||
user: customSession.user,
|
accessToken: customSession.accessToken,
|
||||||
}
|
accessTokenExpires: customSession.accessTokenExpires,
|
||||||
: status === "authenticated" && !customSession?.accessToken
|
user: customSession.user,
|
||||||
? (() => {
|
}
|
||||||
console.warn(
|
: status === "authenticated" && !customSession?.accessToken
|
||||||
"illegal state: authenticated but have no session/or access token. ignoring",
|
? (() => {
|
||||||
);
|
console.warn(
|
||||||
return { status: "unauthenticated" as const };
|
"illegal state: authenticated but have no session/or access token. ignoring",
|
||||||
})()
|
);
|
||||||
: { status: "unauthenticated" as const }),
|
return { status: "unauthenticated" as const };
|
||||||
|
})()
|
||||||
|
: { status: "unauthenticated" as const }),
|
||||||
update,
|
update,
|
||||||
signIn,
|
signIn,
|
||||||
signOut,
|
signOut,
|
||||||
|
|||||||
@@ -18,15 +18,17 @@ export function SessionAutoRefresh({
|
|||||||
const accessTokenExpires =
|
const accessTokenExpires =
|
||||||
auth.status === "authenticated" ? auth.accessTokenExpires : null;
|
auth.status === "authenticated" ? auth.accessTokenExpires : null;
|
||||||
|
|
||||||
|
const refreshIntervalMs = refreshInterval * 1000;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
if (accessTokenExpires) {
|
if (accessTokenExpires !== null) {
|
||||||
const timeLeft = accessTokenExpires - Date.now();
|
const timeLeft = accessTokenExpires - Date.now();
|
||||||
if (timeLeft < refreshInterval * 1000) {
|
if (timeLeft < refreshIntervalMs) {
|
||||||
auth.update();
|
auth.update();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, refreshInterval * 1000);
|
}, refreshIntervalMs);
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [accessTokenExpires, refreshInterval, auth.update]);
|
}, [accessTokenExpires, refreshInterval, auth.update]);
|
||||||
|
|||||||
@@ -8,16 +8,14 @@ import {
|
|||||||
parseMaybeNonEmptyString,
|
parseMaybeNonEmptyString,
|
||||||
} from "./utils";
|
} from "./utils";
|
||||||
|
|
||||||
const PRETIMEOUT = 60; // seconds before token expires to refresh it
|
const PRETIMEOUT = 600;
|
||||||
|
|
||||||
// Simple in-memory cache for tokens (in production, consider using a proper cache solution)
|
|
||||||
const tokenCache = new Map<
|
const tokenCache = new Map<
|
||||||
string,
|
string,
|
||||||
{ token: JWTWithAccessToken; timestamp: number }
|
{ token: JWTWithAccessToken; timestamp: number }
|
||||||
>();
|
>();
|
||||||
const TOKEN_CACHE_TTL = 60 * 60 * 24 * 30 * 1000; // 30 days in milliseconds
|
const TOKEN_CACHE_TTL = 60 * 60 * 24 * 30 * 1000; // 30 days in milliseconds
|
||||||
|
|
||||||
// Simple lock mechanism to prevent concurrent token refreshes
|
|
||||||
const refreshLocks = new Map<string, Promise<JWTWithAccessToken>>();
|
const refreshLocks = new Map<string, Promise<JWTWithAccessToken>>();
|
||||||
|
|
||||||
const CLIENT_ID = assertExistsAndNonEmptyString(
|
const CLIENT_ID = assertExistsAndNonEmptyString(
|
||||||
@@ -100,32 +98,25 @@ async function lockedRefreshAccessToken(
|
|||||||
): Promise<JWTWithAccessToken> {
|
): Promise<JWTWithAccessToken> {
|
||||||
const lockKey = `${token.sub}-refresh`;
|
const lockKey = `${token.sub}-refresh`;
|
||||||
|
|
||||||
// Check if there's already a refresh in progress
|
|
||||||
const existingRefresh = refreshLocks.get(lockKey);
|
const existingRefresh = refreshLocks.get(lockKey);
|
||||||
if (existingRefresh) {
|
if (existingRefresh) {
|
||||||
return existingRefresh;
|
return await existingRefresh;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new refresh promise
|
|
||||||
const refreshPromise = (async () => {
|
const refreshPromise = (async () => {
|
||||||
try {
|
try {
|
||||||
// Check cache for recent token
|
|
||||||
const cached = tokenCache.get(`token:${token.sub}`);
|
const cached = tokenCache.get(`token:${token.sub}`);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
// Clean up old cache entries
|
|
||||||
if (Date.now() - cached.timestamp > TOKEN_CACHE_TTL) {
|
if (Date.now() - cached.timestamp > TOKEN_CACHE_TTL) {
|
||||||
tokenCache.delete(`token:${token.sub}`);
|
tokenCache.delete(`token:${token.sub}`);
|
||||||
} else if (Date.now() < cached.token.accessTokenExpires) {
|
} else if (Date.now() < cached.token.accessTokenExpires) {
|
||||||
// Token is still valid
|
|
||||||
return cached.token;
|
return cached.token;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh the token
|
|
||||||
const currentToken = cached?.token || (token as JWTWithAccessToken);
|
const currentToken = cached?.token || (token as JWTWithAccessToken);
|
||||||
const newToken = await refreshAccessToken(currentToken);
|
const newToken = await refreshAccessToken(currentToken);
|
||||||
|
|
||||||
// Update cache
|
|
||||||
tokenCache.set(`token:${token.sub}`, {
|
tokenCache.set(`token:${token.sub}`, {
|
||||||
token: newToken,
|
token: newToken,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
@@ -133,7 +124,6 @@ async function lockedRefreshAccessToken(
|
|||||||
|
|
||||||
return newToken;
|
return newToken;
|
||||||
} finally {
|
} finally {
|
||||||
// Clean up the lock after a short delay
|
|
||||||
setTimeout(() => refreshLocks.delete(lockKey), 100);
|
setTimeout(() => refreshLocks.delete(lockKey), 100);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|||||||
Reference in New Issue
Block a user