forked from baron/baron-sso
adminfront/devfront 세션 만료 관리 슬라이딩 갱신 로직 추가
This commit is contained in:
@@ -16,9 +16,10 @@ import {
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { useAuth } from "react-oidc-context";
|
import { useAuth } from "react-oidc-context";
|
||||||
import { NavLink, Outlet, useNavigate } from "react-router-dom";
|
import { NavLink, Outlet, useLocation, useNavigate } from "react-router-dom";
|
||||||
import { fetchMe } from "../../lib/adminApi";
|
import { fetchMe } from "../../lib/adminApi";
|
||||||
import { t } from "../../lib/i18n";
|
import { t } from "../../lib/i18n";
|
||||||
|
import { shouldAttemptSlidingSessionRenew } from "../../lib/sessionSliding";
|
||||||
import LanguageSelector from "../common/LanguageSelector";
|
import LanguageSelector from "../common/LanguageSelector";
|
||||||
import RoleSwitcher from "./RoleSwitcher";
|
import RoleSwitcher from "./RoleSwitcher";
|
||||||
|
|
||||||
@@ -32,8 +33,12 @@ const staticNavItems = [
|
|||||||
|
|
||||||
function AppLayout() {
|
function AppLayout() {
|
||||||
const auth = useAuth();
|
const auth = useAuth();
|
||||||
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const profileMenuRef = useRef<HTMLDivElement>(null);
|
const profileMenuRef = useRef<HTMLDivElement>(null);
|
||||||
|
const isRenewInFlightRef = useRef(false);
|
||||||
|
const lastRenewAttemptAtRef = useRef(0);
|
||||||
|
const lastVisitedRouteRef = useRef<string | null>(null);
|
||||||
const isDevRoleOverrideEnabled =
|
const isDevRoleOverrideEnabled =
|
||||||
import.meta.env.MODE === "development" ||
|
import.meta.env.MODE === "development" ||
|
||||||
(window as Window & typeof globalThis & { _IS_TEST_MODE?: boolean })
|
(window as Window & typeof globalThis & { _IS_TEST_MODE?: boolean })
|
||||||
@@ -169,6 +174,104 @@ function AppLayout() {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const maybeRenewSession = async () => {
|
||||||
|
const now = Date.now();
|
||||||
|
if (
|
||||||
|
!shouldAttemptSlidingSessionRenew({
|
||||||
|
expiresAtSec: auth.user?.expires_at,
|
||||||
|
nowMs: now,
|
||||||
|
isEnabled: isSessionExpiryEnabled,
|
||||||
|
isAuthenticated: auth.isAuthenticated,
|
||||||
|
isLoading: auth.isLoading,
|
||||||
|
isRenewInFlight: isRenewInFlightRef.current,
|
||||||
|
lastAttemptAtMs: lastRenewAttemptAtRef.current,
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isRenewInFlightRef.current = true;
|
||||||
|
lastRenewAttemptAtRef.current = now;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await auth.signinSilent();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("세션 자동 연장에 실패했습니다.", error);
|
||||||
|
} finally {
|
||||||
|
isRenewInFlightRef.current = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUserAction = () => {
|
||||||
|
void maybeRenewSession();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("pointerdown", handleUserAction);
|
||||||
|
window.addEventListener("keydown", handleUserAction);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("pointerdown", handleUserAction);
|
||||||
|
window.removeEventListener("keydown", handleUserAction);
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
auth,
|
||||||
|
auth.isAuthenticated,
|
||||||
|
auth.isLoading,
|
||||||
|
auth.user?.expires_at,
|
||||||
|
isSessionExpiryEnabled,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const routeKey = `${location.pathname}${location.search}${location.hash}`;
|
||||||
|
if (lastVisitedRouteRef.current === null) {
|
||||||
|
lastVisitedRouteRef.current = routeKey;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastVisitedRouteRef.current === routeKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastVisitedRouteRef.current = routeKey;
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
if (
|
||||||
|
!shouldAttemptSlidingSessionRenew({
|
||||||
|
expiresAtSec: auth.user?.expires_at,
|
||||||
|
nowMs: now,
|
||||||
|
isEnabled: isSessionExpiryEnabled,
|
||||||
|
isAuthenticated: auth.isAuthenticated,
|
||||||
|
isLoading: auth.isLoading,
|
||||||
|
isRenewInFlight: isRenewInFlightRef.current,
|
||||||
|
lastAttemptAtMs: lastRenewAttemptAtRef.current,
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isRenewInFlightRef.current = true;
|
||||||
|
lastRenewAttemptAtRef.current = now;
|
||||||
|
|
||||||
|
void auth
|
||||||
|
.signinSilent()
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("세션 자동 연장에 실패했습니다.", error);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
isRenewInFlightRef.current = false;
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
auth,
|
||||||
|
auth.isAuthenticated,
|
||||||
|
auth.isLoading,
|
||||||
|
auth.user?.expires_at,
|
||||||
|
isSessionExpiryEnabled,
|
||||||
|
location.hash,
|
||||||
|
location.pathname,
|
||||||
|
location.search,
|
||||||
|
]);
|
||||||
|
|
||||||
const toggleTheme = () => {
|
const toggleTheme = () => {
|
||||||
setTheme((prev) => (prev === "light" ? "dark" : "light"));
|
setTheme((prev) => (prev === "light" ? "dark" : "light"));
|
||||||
};
|
};
|
||||||
@@ -397,14 +500,14 @@ function AppLayout() {
|
|||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-foreground">
|
<p className="text-sm font-medium text-foreground">
|
||||||
{t("ui.dev.session.expired", "세션 만료")}
|
{t("ui.dev.session.auto_extend", "세션 만료 관리")}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{isSessionExpiryEnabled
|
{isSessionExpiryEnabled
|
||||||
? sessionText
|
? sessionText
|
||||||
: t(
|
: t(
|
||||||
"ui.dev.session.disabled",
|
"ui.dev.session.disabled",
|
||||||
"세션 만료 표시 비활성화",
|
"세션 만료 비활성화",
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
65
adminfront/src/lib/sessionSliding.test.ts
Normal file
65
adminfront/src/lib/sessionSliding.test.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
SESSION_RENEW_THRESHOLD_MS,
|
||||||
|
shouldAttemptSlidingSessionRenew,
|
||||||
|
} from "./sessionSliding";
|
||||||
|
|
||||||
|
describe("shouldAttemptSlidingSessionRenew", () => {
|
||||||
|
const nowMs = 1_700_000_000_000;
|
||||||
|
|
||||||
|
it("returns false when remaining time is above the 5 minute threshold", () => {
|
||||||
|
expect(
|
||||||
|
shouldAttemptSlidingSessionRenew({
|
||||||
|
expiresAtSec: Math.floor((nowMs + SESSION_RENEW_THRESHOLD_MS + 1_000) / 1000),
|
||||||
|
nowMs,
|
||||||
|
isEnabled: true,
|
||||||
|
isAuthenticated: true,
|
||||||
|
isLoading: false,
|
||||||
|
isRenewInFlight: false,
|
||||||
|
lastAttemptAtMs: 0,
|
||||||
|
}),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns true when remaining time is within the 5 minute threshold", () => {
|
||||||
|
expect(
|
||||||
|
shouldAttemptSlidingSessionRenew({
|
||||||
|
expiresAtSec: Math.floor((nowMs + SESSION_RENEW_THRESHOLD_MS - 1_000) / 1000),
|
||||||
|
nowMs,
|
||||||
|
isEnabled: true,
|
||||||
|
isAuthenticated: true,
|
||||||
|
isLoading: false,
|
||||||
|
isRenewInFlight: false,
|
||||||
|
lastAttemptAtMs: 0,
|
||||||
|
}),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false when automatic renewal is disabled", () => {
|
||||||
|
expect(
|
||||||
|
shouldAttemptSlidingSessionRenew({
|
||||||
|
expiresAtSec: Math.floor((nowMs + SESSION_RENEW_THRESHOLD_MS - 1_000) / 1000),
|
||||||
|
nowMs,
|
||||||
|
isEnabled: false,
|
||||||
|
isAuthenticated: true,
|
||||||
|
isLoading: false,
|
||||||
|
isRenewInFlight: false,
|
||||||
|
lastAttemptAtMs: 0,
|
||||||
|
}),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false when the last renew attempt is still within the throttle window", () => {
|
||||||
|
expect(
|
||||||
|
shouldAttemptSlidingSessionRenew({
|
||||||
|
expiresAtSec: Math.floor((nowMs + SESSION_RENEW_THRESHOLD_MS - 1_000) / 1000),
|
||||||
|
nowMs,
|
||||||
|
isEnabled: true,
|
||||||
|
isAuthenticated: true,
|
||||||
|
isLoading: false,
|
||||||
|
isRenewInFlight: false,
|
||||||
|
lastAttemptAtMs: nowMs - 10_000,
|
||||||
|
}),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
45
adminfront/src/lib/sessionSliding.ts
Normal file
45
adminfront/src/lib/sessionSliding.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
export const SESSION_RENEW_THRESHOLD_MS = 5 * 60 * 1000;
|
||||||
|
export const SESSION_RENEW_THROTTLE_MS = 30 * 1000;
|
||||||
|
|
||||||
|
type SlidingSessionRenewDecisionParams = {
|
||||||
|
expiresAtSec?: number | null;
|
||||||
|
nowMs: number;
|
||||||
|
isEnabled: boolean;
|
||||||
|
isAuthenticated: boolean;
|
||||||
|
isLoading: boolean;
|
||||||
|
isRenewInFlight: boolean;
|
||||||
|
lastAttemptAtMs: number;
|
||||||
|
thresholdMs?: number;
|
||||||
|
throttleMs?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function shouldAttemptSlidingSessionRenew({
|
||||||
|
expiresAtSec,
|
||||||
|
nowMs,
|
||||||
|
isEnabled,
|
||||||
|
isAuthenticated,
|
||||||
|
isLoading,
|
||||||
|
isRenewInFlight,
|
||||||
|
lastAttemptAtMs,
|
||||||
|
thresholdMs = SESSION_RENEW_THRESHOLD_MS,
|
||||||
|
throttleMs = SESSION_RENEW_THROTTLE_MS,
|
||||||
|
}: SlidingSessionRenewDecisionParams) {
|
||||||
|
if (!isEnabled || !isAuthenticated || isLoading || isRenewInFlight) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof expiresAtSec !== "number") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const remainingMs = expiresAtSec * 1000 - nowMs;
|
||||||
|
if (remainingMs <= 0 || remainingMs > thresholdMs) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nowMs - lastAttemptAtMs < throttleMs) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -1466,8 +1466,9 @@ plane = "Dev Plane"
|
|||||||
subtitle = "Manage your applications"
|
subtitle = "Manage your applications"
|
||||||
|
|
||||||
[ui.dev.session]
|
[ui.dev.session]
|
||||||
|
auto_extend = "Session expiry"
|
||||||
active = "Session active"
|
active = "Session active"
|
||||||
disabled = "Session expiry display disabled"
|
disabled = "Session expiry disabled"
|
||||||
unknown = "Unknown"
|
unknown = "Unknown"
|
||||||
expired = "Session expired"
|
expired = "Session expired"
|
||||||
expiring = "Expiring soon: {{minutes}}m {{seconds}}s left"
|
expiring = "Expiring soon: {{minutes}}m {{seconds}}s left"
|
||||||
|
|||||||
@@ -1465,8 +1465,9 @@ plane = "Dev Plane"
|
|||||||
subtitle = "Manage your applications"
|
subtitle = "Manage your applications"
|
||||||
|
|
||||||
[ui.dev.session]
|
[ui.dev.session]
|
||||||
|
auto_extend = "세션 만료 관리"
|
||||||
active = "세션 활성"
|
active = "세션 활성"
|
||||||
disabled = "세션 만료 표시 비활성화"
|
disabled = "세션 만료 비활성화"
|
||||||
unknown = "알 수 없음"
|
unknown = "알 수 없음"
|
||||||
expired = "세션 만료"
|
expired = "세션 만료"
|
||||||
expiring = "만료 임박: {{minutes}}분 {{seconds}}초 남음"
|
expiring = "만료 임박: {{minutes}}분 {{seconds}}초 남음"
|
||||||
|
|||||||
@@ -1465,6 +1465,7 @@ plane = ""
|
|||||||
subtitle = ""
|
subtitle = ""
|
||||||
|
|
||||||
[ui.dev.session]
|
[ui.dev.session]
|
||||||
|
auto_extend = ""
|
||||||
active = ""
|
active = ""
|
||||||
disabled = ""
|
disabled = ""
|
||||||
unknown = ""
|
unknown = ""
|
||||||
|
|||||||
@@ -11,10 +11,11 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { useAuth } from "react-oidc-context";
|
import { useAuth } from "react-oidc-context";
|
||||||
import { NavLink, Outlet, useNavigate } from "react-router-dom";
|
import { NavLink, Outlet, useLocation, useNavigate } from "react-router-dom";
|
||||||
import { fetchMe } from "../../features/auth/authApi";
|
import { fetchMe } from "../../features/auth/authApi";
|
||||||
import { t } from "../../lib/i18n";
|
import { t } from "../../lib/i18n";
|
||||||
import { resolveProfileRole } from "../../lib/role";
|
import { resolveProfileRole } from "../../lib/role";
|
||||||
|
import { shouldAttemptSlidingSessionRenew } from "../../lib/sessionSliding";
|
||||||
import LanguageSelector from "../common/LanguageSelector";
|
import LanguageSelector from "../common/LanguageSelector";
|
||||||
import { Toaster } from "../ui/toaster";
|
import { Toaster } from "../ui/toaster";
|
||||||
|
|
||||||
@@ -35,8 +36,12 @@ const navItems = [
|
|||||||
|
|
||||||
function AppLayout() {
|
function AppLayout() {
|
||||||
const auth = useAuth();
|
const auth = useAuth();
|
||||||
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const profileMenuRef = useRef<HTMLDivElement>(null);
|
const profileMenuRef = useRef<HTMLDivElement>(null);
|
||||||
|
const isRenewInFlightRef = useRef(false);
|
||||||
|
const lastRenewAttemptAtRef = useRef(0);
|
||||||
|
const lastVisitedRouteRef = useRef<string | null>(null);
|
||||||
const [theme, setTheme] = useState<"light" | "dark">(() => {
|
const [theme, setTheme] = useState<"light" | "dark">(() => {
|
||||||
const stored = window.localStorage.getItem("admin_theme");
|
const stored = window.localStorage.getItem("admin_theme");
|
||||||
return stored === "dark" ? "dark" : "light";
|
return stored === "dark" ? "dark" : "light";
|
||||||
@@ -98,6 +103,104 @@ function AppLayout() {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const maybeRenewSession = async () => {
|
||||||
|
const now = Date.now();
|
||||||
|
if (
|
||||||
|
!shouldAttemptSlidingSessionRenew({
|
||||||
|
expiresAtSec: auth.user?.expires_at,
|
||||||
|
nowMs: now,
|
||||||
|
isEnabled: isSessionExpiryEnabled,
|
||||||
|
isAuthenticated: auth.isAuthenticated,
|
||||||
|
isLoading: auth.isLoading,
|
||||||
|
isRenewInFlight: isRenewInFlightRef.current,
|
||||||
|
lastAttemptAtMs: lastRenewAttemptAtRef.current,
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isRenewInFlightRef.current = true;
|
||||||
|
lastRenewAttemptAtRef.current = now;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await auth.signinSilent();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("세션 자동 연장에 실패했습니다.", error);
|
||||||
|
} finally {
|
||||||
|
isRenewInFlightRef.current = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUserAction = () => {
|
||||||
|
void maybeRenewSession();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("pointerdown", handleUserAction);
|
||||||
|
window.addEventListener("keydown", handleUserAction);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("pointerdown", handleUserAction);
|
||||||
|
window.removeEventListener("keydown", handleUserAction);
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
auth,
|
||||||
|
auth.isAuthenticated,
|
||||||
|
auth.isLoading,
|
||||||
|
auth.user?.expires_at,
|
||||||
|
isSessionExpiryEnabled,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const routeKey = `${location.pathname}${location.search}${location.hash}`;
|
||||||
|
if (lastVisitedRouteRef.current === null) {
|
||||||
|
lastVisitedRouteRef.current = routeKey;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastVisitedRouteRef.current === routeKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastVisitedRouteRef.current = routeKey;
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
if (
|
||||||
|
!shouldAttemptSlidingSessionRenew({
|
||||||
|
expiresAtSec: auth.user?.expires_at,
|
||||||
|
nowMs: now,
|
||||||
|
isEnabled: isSessionExpiryEnabled,
|
||||||
|
isAuthenticated: auth.isAuthenticated,
|
||||||
|
isLoading: auth.isLoading,
|
||||||
|
isRenewInFlight: isRenewInFlightRef.current,
|
||||||
|
lastAttemptAtMs: lastRenewAttemptAtRef.current,
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isRenewInFlightRef.current = true;
|
||||||
|
lastRenewAttemptAtRef.current = now;
|
||||||
|
|
||||||
|
void auth
|
||||||
|
.signinSilent()
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("세션 자동 연장에 실패했습니다.", error);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
isRenewInFlightRef.current = false;
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
auth,
|
||||||
|
auth.isAuthenticated,
|
||||||
|
auth.isLoading,
|
||||||
|
auth.user?.expires_at,
|
||||||
|
isSessionExpiryEnabled,
|
||||||
|
location.hash,
|
||||||
|
location.pathname,
|
||||||
|
location.search,
|
||||||
|
]);
|
||||||
|
|
||||||
const toggleTheme = () => {
|
const toggleTheme = () => {
|
||||||
setTheme((prev) => (prev === "light" ? "dark" : "light"));
|
setTheme((prev) => (prev === "light" ? "dark" : "light"));
|
||||||
};
|
};
|
||||||
@@ -346,14 +449,14 @@ function AppLayout() {
|
|||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-foreground">
|
<p className="text-sm font-medium text-foreground">
|
||||||
{t("ui.dev.session.expired", "세션 만료")}
|
{t("ui.dev.session.auto_extend", "세션 만료 관리")}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{isSessionExpiryEnabled
|
{isSessionExpiryEnabled
|
||||||
? sessionText
|
? sessionText
|
||||||
: t(
|
: t(
|
||||||
"ui.dev.session.disabled",
|
"ui.dev.session.disabled",
|
||||||
"세션 만료 표시 비활성화",
|
"세션 만료 비활성화",
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
45
devfront/src/lib/sessionSliding.ts
Normal file
45
devfront/src/lib/sessionSliding.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
export const SESSION_RENEW_THRESHOLD_MS = 5 * 60 * 1000;
|
||||||
|
export const SESSION_RENEW_THROTTLE_MS = 30 * 1000;
|
||||||
|
|
||||||
|
type SlidingSessionRenewDecisionParams = {
|
||||||
|
expiresAtSec?: number | null;
|
||||||
|
nowMs: number;
|
||||||
|
isEnabled: boolean;
|
||||||
|
isAuthenticated: boolean;
|
||||||
|
isLoading: boolean;
|
||||||
|
isRenewInFlight: boolean;
|
||||||
|
lastAttemptAtMs: number;
|
||||||
|
thresholdMs?: number;
|
||||||
|
throttleMs?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function shouldAttemptSlidingSessionRenew({
|
||||||
|
expiresAtSec,
|
||||||
|
nowMs,
|
||||||
|
isEnabled,
|
||||||
|
isAuthenticated,
|
||||||
|
isLoading,
|
||||||
|
isRenewInFlight,
|
||||||
|
lastAttemptAtMs,
|
||||||
|
thresholdMs = SESSION_RENEW_THRESHOLD_MS,
|
||||||
|
throttleMs = SESSION_RENEW_THROTTLE_MS,
|
||||||
|
}: SlidingSessionRenewDecisionParams) {
|
||||||
|
if (!isEnabled || !isAuthenticated || isLoading || isRenewInFlight) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof expiresAtSec !== "number") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const remainingMs = expiresAtSec * 1000 - nowMs;
|
||||||
|
if (remainingMs <= 0 || remainingMs > thresholdMs) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nowMs - lastAttemptAtMs < throttleMs) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -1495,8 +1495,9 @@ plane = "Dev Plane"
|
|||||||
subtitle = "Manage your applications"
|
subtitle = "Manage your applications"
|
||||||
|
|
||||||
[ui.dev.session]
|
[ui.dev.session]
|
||||||
|
auto_extend = "Session expiry"
|
||||||
active = "Session active"
|
active = "Session active"
|
||||||
disabled = "Session expiry display disabled"
|
disabled = "Session expiry disabled"
|
||||||
unknown = "Unknown"
|
unknown = "Unknown"
|
||||||
expired = "Session expired"
|
expired = "Session expired"
|
||||||
expiring = "Expiring soon: {{minutes}}m {{seconds}}s left"
|
expiring = "Expiring soon: {{minutes}}m {{seconds}}s left"
|
||||||
|
|||||||
@@ -1495,8 +1495,9 @@ plane = "Dev Plane"
|
|||||||
subtitle = "Manage your applications"
|
subtitle = "Manage your applications"
|
||||||
|
|
||||||
[ui.dev.session]
|
[ui.dev.session]
|
||||||
|
auto_extend = "세션 만료 관리"
|
||||||
active = "세션 활성"
|
active = "세션 활성"
|
||||||
disabled = "세션 만료 표시 비활성화"
|
disabled = "세션 만료 비활성화"
|
||||||
unknown = "알 수 없음"
|
unknown = "알 수 없음"
|
||||||
expired = "세션 만료"
|
expired = "세션 만료"
|
||||||
expiring = "만료 임박: {{minutes}}분 {{seconds}}초 남음"
|
expiring = "만료 임박: {{minutes}}분 {{seconds}}초 남음"
|
||||||
|
|||||||
@@ -1493,6 +1493,7 @@ plane = ""
|
|||||||
subtitle = ""
|
subtitle = ""
|
||||||
|
|
||||||
[ui.dev.session]
|
[ui.dev.session]
|
||||||
|
auto_extend = ""
|
||||||
active = ""
|
active = ""
|
||||||
disabled = ""
|
disabled = ""
|
||||||
unknown = ""
|
unknown = ""
|
||||||
|
|||||||
Reference in New Issue
Block a user