forked from baron/baron-sso
front류 개발모드에서는 세션 갱신 끄기
This commit is contained in:
@@ -2,6 +2,7 @@ import { useState } from "react";
|
||||
import { t } from "../../lib/i18n";
|
||||
|
||||
const LOCALE_STORAGE_KEY = "locale";
|
||||
const LOCALE_CHANGED_EVENT = "baron_locale_changed";
|
||||
const SUPPORTED_LOCALES = ["ko", "en"] as const;
|
||||
|
||||
type Locale = (typeof SUPPORTED_LOCALES)[number];
|
||||
@@ -34,6 +35,10 @@ function LanguageSelector() {
|
||||
}
|
||||
window.localStorage.setItem(LOCALE_STORAGE_KEY, next);
|
||||
setLocale(next);
|
||||
if (import.meta.env.MODE === "development") {
|
||||
window.dispatchEvent(new Event(LOCALE_CHANGED_EVENT));
|
||||
return;
|
||||
}
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
|
||||
@@ -43,6 +43,9 @@ import {
|
||||
import LanguageSelector from "../common/LanguageSelector";
|
||||
import RoleSwitcher from "./RoleSwitcher";
|
||||
|
||||
const LOCALE_CHANGED_EVENT = "baron_locale_changed";
|
||||
const DEV_ROLE_CHANGED_EVENT = "baron_dev_role_changed";
|
||||
|
||||
const staticNavItems: ShellSidebarNavItem[] = [
|
||||
{
|
||||
labelKey: "ui.admin.nav.overview",
|
||||
@@ -127,6 +130,7 @@ function AppLayout() {
|
||||
const isRenewInFlightRef = useRef(false);
|
||||
const lastRenewAttemptAtRef = useRef(0);
|
||||
const lastVisitedRouteRef = useRef<string | null>(null);
|
||||
const isDevelopmentRuntime = import.meta.env.MODE === "development";
|
||||
const isDevRoleOverrideEnabled =
|
||||
import.meta.env.MODE === "development" ||
|
||||
(window as Window & typeof globalThis & { _IS_TEST_MODE?: boolean })
|
||||
@@ -139,8 +143,9 @@ function AppLayout() {
|
||||
: null;
|
||||
const [theme, setTheme] = useState<"light" | "dark">(readShellTheme);
|
||||
const [isProfileOpen, setIsProfileOpen] = useState(false);
|
||||
const [isSessionExpiryEnabled, setIsSessionExpiryEnabled] = useState(
|
||||
readShellSessionExpiryEnabled,
|
||||
const [, setDevelopmentRenderRevision] = useState(0);
|
||||
const [isSessionExpiryEnabled, setIsSessionExpiryEnabled] = useState(() =>
|
||||
readShellSessionExpiryEnabled(!isDevelopmentRuntime),
|
||||
);
|
||||
const {
|
||||
data: profile,
|
||||
@@ -290,6 +295,27 @@ function AppLayout() {
|
||||
applyShellTheme(theme);
|
||||
}, [theme]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDevelopmentRuntime) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rerenderDevelopmentShell = () => {
|
||||
setDevelopmentRenderRevision((value) => value + 1);
|
||||
};
|
||||
|
||||
window.addEventListener(LOCALE_CHANGED_EVENT, rerenderDevelopmentShell);
|
||||
window.addEventListener(DEV_ROLE_CHANGED_EVENT, rerenderDevelopmentShell);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener(LOCALE_CHANGED_EVENT, rerenderDevelopmentShell);
|
||||
window.removeEventListener(
|
||||
DEV_ROLE_CHANGED_EVENT,
|
||||
rerenderDevelopmentShell,
|
||||
);
|
||||
};
|
||||
}, [isDevelopmentRuntime]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
@@ -355,6 +381,10 @@ function AppLayout() {
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isDevelopmentRuntime) {
|
||||
return;
|
||||
}
|
||||
|
||||
const maybeKeepSessionAlive = async () => {
|
||||
const now = Date.now();
|
||||
if (
|
||||
@@ -397,6 +427,7 @@ function AppLayout() {
|
||||
auth.isAuthenticated,
|
||||
auth.isLoading,
|
||||
auth.user?.expires_at,
|
||||
isDevelopmentRuntime,
|
||||
isSessionExpiryEnabled,
|
||||
]);
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ import type { FC } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { t } from "../../lib/i18n";
|
||||
|
||||
const DEV_ROLE_CHANGED_EVENT = "baron_dev_role_changed";
|
||||
|
||||
const RoleSwitcher: FC = () => {
|
||||
const [currentRole, setCurrentRole] = useState<string>("");
|
||||
const [isOverrideEnabled, setIsOverrideEnabled] = useState<boolean>(false);
|
||||
@@ -31,13 +33,13 @@ const RoleSwitcher: FC = () => {
|
||||
window.localStorage.setItem("X-Mock-Role-Enabled", "true");
|
||||
setCurrentRole(role);
|
||||
setIsOverrideEnabled(true);
|
||||
window.location.reload();
|
||||
window.dispatchEvent(new Event(DEV_ROLE_CHANGED_EVENT));
|
||||
};
|
||||
|
||||
const clearRoleOverride = () => {
|
||||
window.localStorage.removeItem("X-Mock-Role-Enabled");
|
||||
setIsOverrideEnabled(false);
|
||||
window.location.reload();
|
||||
window.dispatchEvent(new Event(DEV_ROLE_CHANGED_EVENT));
|
||||
};
|
||||
|
||||
if (import.meta.env.MODE === "production") return null;
|
||||
|
||||
@@ -84,8 +84,11 @@ function LoginPage() {
|
||||
variant="ghost"
|
||||
className="p-0 h-auto text-destructive underline mt-2 hover:bg-transparent"
|
||||
onClick={() => {
|
||||
window.location.href =
|
||||
window.location.origin + window.location.pathname;
|
||||
void auth.signinRedirect({
|
||||
state: {
|
||||
returnTo,
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
다시 시도하기
|
||||
|
||||
@@ -194,7 +194,14 @@ export function UserGroupDetailPage() {
|
||||
"Not found"}
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="outline" onClick={() => window.location.reload()}>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: ["user-group-detail", id],
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("ui.common.retry", "다시 시도")}
|
||||
</Button>
|
||||
<div className="pt-4 border-t">
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import axios from "axios";
|
||||
import { shouldStartLoginRedirect } from "../../../common/core/auth";
|
||||
import {
|
||||
shouldSuppressDevelopmentSessionRedirect,
|
||||
} from "../../../common/core/session";
|
||||
import { userManager } from "./auth";
|
||||
|
||||
let isRedirectingToLogin = false;
|
||||
@@ -42,6 +45,17 @@ apiClient.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error) => {
|
||||
if (error.response?.status === 401) {
|
||||
if (
|
||||
shouldSuppressDevelopmentSessionRedirect({
|
||||
appMode: import.meta.env.MODE,
|
||||
})
|
||||
) {
|
||||
console.warn(
|
||||
"[apiClient] 401 Unauthorized detected, but development session redirects are disabled.",
|
||||
);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
console.warn(
|
||||
"[apiClient] 401 Unauthorized detected. Clearing session state.",
|
||||
);
|
||||
|
||||
@@ -1,10 +1,23 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
readSessionExpiryEnabled,
|
||||
SESSION_RENEW_THRESHOLD_MS,
|
||||
shouldAttemptSlidingSessionRenew,
|
||||
shouldSuppressDevelopmentSessionRedirect,
|
||||
shouldAttemptUnlimitedSessionRenew,
|
||||
writeSessionExpiryEnabled,
|
||||
} from "./sessionSliding";
|
||||
|
||||
function memoryStorage(initialValue: string | null = null) {
|
||||
let value = initialValue;
|
||||
return {
|
||||
getItem: (_key: string) => value,
|
||||
setItem: (_key: string, nextValue: string) => {
|
||||
value = nextValue;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("shouldAttemptSlidingSessionRenew", () => {
|
||||
const nowMs = 1_700_000_000_000;
|
||||
|
||||
@@ -124,3 +137,43 @@ describe("shouldAttemptUnlimitedSessionRenew", () => {
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("session expiry development preference", () => {
|
||||
it("defaults session expiry management off in development when no preference is stored", () => {
|
||||
expect(
|
||||
readSessionExpiryEnabled({
|
||||
defaultEnabled: false,
|
||||
storage: memoryStorage(null),
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps explicit stored preference over the development default", () => {
|
||||
const storage = memoryStorage(null);
|
||||
|
||||
writeSessionExpiryEnabled(true, storage);
|
||||
|
||||
expect(
|
||||
readSessionExpiryEnabled({
|
||||
defaultEnabled: false,
|
||||
storage,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("suppresses login redirects only in development with disabled session expiry management", () => {
|
||||
expect(
|
||||
shouldSuppressDevelopmentSessionRedirect({
|
||||
appMode: "development",
|
||||
storage: memoryStorage(null),
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
shouldSuppressDevelopmentSessionRedirect({
|
||||
appMode: "production",
|
||||
storage: memoryStorage(null),
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
export {
|
||||
DEFAULT_SESSION_RENEW_THROTTLE_MS as SESSION_RENEW_THROTTLE_MS,
|
||||
DEFAULT_SESSION_RENEW_THRESHOLD_MS as SESSION_RENEW_THRESHOLD_MS,
|
||||
readSessionExpiryEnabled,
|
||||
shouldAttemptSlidingSessionRenew,
|
||||
shouldSuppressDevelopmentSessionRedirect,
|
||||
shouldAttemptUnlimitedSessionRenew,
|
||||
writeSessionExpiryEnabled,
|
||||
} from "../../../common/core/session";
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
export const DEFAULT_SESSION_RENEW_THRESHOLD_MS = 10 * 60 * 1000;
|
||||
export const DEFAULT_SESSION_RENEW_THROTTLE_MS = 30 * 1000;
|
||||
export const SESSION_EXPIRY_STORAGE_KEY = "baron_session_expiry_enabled";
|
||||
|
||||
type SessionExpiryReadableStorage = Pick<Storage, "getItem">;
|
||||
type SessionExpiryWritableStorage = Pick<Storage, "setItem">;
|
||||
|
||||
export type SessionRenewDecisionParams = {
|
||||
expiresAtSec?: number | null;
|
||||
@@ -13,6 +17,51 @@ export type SessionRenewDecisionParams = {
|
||||
throttleMs?: number;
|
||||
};
|
||||
|
||||
export type SessionExpiryPreferenceParams = {
|
||||
defaultEnabled?: boolean;
|
||||
storage?: SessionExpiryReadableStorage | null;
|
||||
};
|
||||
|
||||
export type DevelopmentSessionRedirectParams = {
|
||||
appMode: string;
|
||||
defaultEnabled?: boolean;
|
||||
storage?: SessionExpiryReadableStorage | null;
|
||||
};
|
||||
|
||||
function browserStorage() {
|
||||
if (typeof window === "undefined") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return window.localStorage;
|
||||
}
|
||||
|
||||
export function readSessionExpiryEnabled({
|
||||
defaultEnabled = true,
|
||||
storage = browserStorage(),
|
||||
}: SessionExpiryPreferenceParams = {}) {
|
||||
const stored = storage?.getItem(SESSION_EXPIRY_STORAGE_KEY) ?? null;
|
||||
return stored === null ? defaultEnabled : stored !== "false";
|
||||
}
|
||||
|
||||
export function writeSessionExpiryEnabled(
|
||||
isEnabled: boolean,
|
||||
storage: SessionExpiryWritableStorage | null = browserStorage(),
|
||||
) {
|
||||
storage?.setItem(SESSION_EXPIRY_STORAGE_KEY, String(isEnabled));
|
||||
}
|
||||
|
||||
export function shouldSuppressDevelopmentSessionRedirect({
|
||||
appMode,
|
||||
defaultEnabled = appMode !== "development",
|
||||
storage = browserStorage(),
|
||||
}: DevelopmentSessionRedirectParams) {
|
||||
return (
|
||||
appMode === "development" &&
|
||||
!readSessionExpiryEnabled({ defaultEnabled, storage })
|
||||
);
|
||||
}
|
||||
|
||||
function hasRenewPreconditions({
|
||||
isAuthenticated,
|
||||
isLoading,
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import {
|
||||
SESSION_EXPIRY_STORAGE_KEY,
|
||||
readSessionExpiryEnabled,
|
||||
writeSessionExpiryEnabled,
|
||||
} from "../core/session";
|
||||
|
||||
export type ShellTheme = "light" | "dark";
|
||||
|
||||
export type ShellTranslator = (
|
||||
@@ -20,8 +26,7 @@ type ShellProfileSummaryParams = {
|
||||
};
|
||||
|
||||
export const SHELL_THEME_STORAGE_KEY = "admin_theme";
|
||||
export const SHELL_SESSION_EXPIRY_STORAGE_KEY =
|
||||
"baron_session_expiry_enabled";
|
||||
export const SHELL_SESSION_EXPIRY_STORAGE_KEY = SESSION_EXPIRY_STORAGE_KEY;
|
||||
export { AppSidebar } from "./AppSidebar";
|
||||
export type { ShellSidebarNavItem } from "./AppSidebar";
|
||||
export { shellLayoutClasses } from "./layout";
|
||||
@@ -39,15 +44,12 @@ export function applyShellTheme(theme: ShellTheme) {
|
||||
window.localStorage.setItem(SHELL_THEME_STORAGE_KEY, theme);
|
||||
}
|
||||
|
||||
export function readShellSessionExpiryEnabled() {
|
||||
return window.localStorage.getItem(SHELL_SESSION_EXPIRY_STORAGE_KEY) !== "false";
|
||||
export function readShellSessionExpiryEnabled(defaultEnabled = true) {
|
||||
return readSessionExpiryEnabled({ defaultEnabled });
|
||||
}
|
||||
|
||||
export function writeShellSessionExpiryEnabled(isEnabled: boolean) {
|
||||
window.localStorage.setItem(
|
||||
SHELL_SESSION_EXPIRY_STORAGE_KEY,
|
||||
String(isEnabled),
|
||||
);
|
||||
writeSessionExpiryEnabled(isEnabled);
|
||||
}
|
||||
|
||||
export function buildShellProfileSummary({
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState } from "react";
|
||||
import { t } from "../../lib/i18n";
|
||||
|
||||
const LOCALE_STORAGE_KEY = "locale";
|
||||
const LOCALE_CHANGED_EVENT = "baron_locale_changed";
|
||||
const SUPPORTED_LOCALES = ["ko", "en"] as const;
|
||||
|
||||
type Locale = (typeof SUPPORTED_LOCALES)[number];
|
||||
@@ -34,6 +35,10 @@ function LanguageSelector() {
|
||||
}
|
||||
window.localStorage.setItem(LOCALE_STORAGE_KEY, next);
|
||||
setLocale(next);
|
||||
if (import.meta.env.MODE === "development") {
|
||||
window.dispatchEvent(new Event(LOCALE_CHANGED_EVENT));
|
||||
return;
|
||||
}
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
|
||||
@@ -35,6 +35,8 @@ import {
|
||||
import LanguageSelector from "../common/LanguageSelector";
|
||||
import { Toaster } from "../ui/toaster";
|
||||
|
||||
const LOCALE_CHANGED_EVENT = "baron_locale_changed";
|
||||
|
||||
const navItems: ShellSidebarNavItem[] = [
|
||||
{
|
||||
labelKey: "ui.dev.nav.overview",
|
||||
@@ -113,10 +115,12 @@ function AppLayout() {
|
||||
const isRenewInFlightRef = useRef(false);
|
||||
const lastRenewAttemptAtRef = useRef(0);
|
||||
const lastVisitedRouteRef = useRef<string | null>(null);
|
||||
const isDevelopmentRuntime = import.meta.env.MODE === "development";
|
||||
const [theme, setTheme] = useState<"light" | "dark">(readShellTheme);
|
||||
const [isProfileMenuOpen, setIsProfileMenuOpen] = useState(false);
|
||||
const [isSessionExpiryEnabled, setIsSessionExpiryEnabled] = useState(
|
||||
readShellSessionExpiryEnabled,
|
||||
const [, setDevelopmentRenderRevision] = useState(0);
|
||||
const [isSessionExpiryEnabled, setIsSessionExpiryEnabled] = useState(() =>
|
||||
readShellSessionExpiryEnabled(!isDevelopmentRuntime),
|
||||
);
|
||||
const hasAccessToken = Boolean(auth.user?.access_token);
|
||||
const { data: profile } = useQuery({
|
||||
@@ -140,6 +144,22 @@ function AppLayout() {
|
||||
applyShellTheme(theme);
|
||||
}, [theme]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDevelopmentRuntime) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rerenderDevelopmentShell = () => {
|
||||
setDevelopmentRenderRevision((value) => value + 1);
|
||||
};
|
||||
|
||||
window.addEventListener(LOCALE_CHANGED_EVENT, rerenderDevelopmentShell);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener(LOCALE_CHANGED_EVENT, rerenderDevelopmentShell);
|
||||
};
|
||||
}, [isDevelopmentRuntime]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
@@ -205,6 +225,10 @@ function AppLayout() {
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isDevelopmentRuntime) {
|
||||
return;
|
||||
}
|
||||
|
||||
const maybeKeepSessionAlive = async () => {
|
||||
const now = Date.now();
|
||||
if (
|
||||
@@ -247,6 +271,7 @@ function AppLayout() {
|
||||
auth.isAuthenticated,
|
||||
auth.isLoading,
|
||||
auth.user?.expires_at,
|
||||
isDevelopmentRuntime,
|
||||
isSessionExpiryEnabled,
|
||||
]);
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import axios from "axios";
|
||||
import { shouldStartLoginRedirect } from "../../../common/core/auth";
|
||||
import {
|
||||
shouldSuppressDevelopmentSessionRedirect,
|
||||
} from "../../../common/core/session";
|
||||
import { userManager } from "./auth";
|
||||
|
||||
let isRedirectingToLogin = false;
|
||||
@@ -42,8 +45,22 @@ apiClient.interceptors.response.use(
|
||||
message.includes("invalid session") ||
|
||||
message.includes("token is not active")));
|
||||
|
||||
if (!shouldRedirectToLogin) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
if (
|
||||
shouldSuppressDevelopmentSessionRedirect({
|
||||
appMode: import.meta.env.MODE,
|
||||
})
|
||||
) {
|
||||
console.warn(
|
||||
"[apiClient] Auth failure detected, but development session redirects are disabled.",
|
||||
);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
if (
|
||||
shouldRedirectToLogin &&
|
||||
shouldStartLoginRedirect({
|
||||
pathname: window.location.pathname,
|
||||
isRedirecting: isRedirectingToLogin,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState } from "react";
|
||||
import { t } from "../../lib/i18n";
|
||||
|
||||
const LOCALE_STORAGE_KEY = "locale";
|
||||
const LOCALE_CHANGED_EVENT = "baron_locale_changed";
|
||||
const SUPPORTED_LOCALES = ["ko", "en"] as const;
|
||||
|
||||
type Locale = (typeof SUPPORTED_LOCALES)[number];
|
||||
@@ -34,6 +35,10 @@ function LanguageSelector() {
|
||||
}
|
||||
window.localStorage.setItem(LOCALE_STORAGE_KEY, next);
|
||||
setLocale(next);
|
||||
if (import.meta.env.MODE === "development") {
|
||||
window.dispatchEvent(new Event(LOCALE_CHANGED_EVENT));
|
||||
return;
|
||||
}
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
|
||||
@@ -32,6 +32,8 @@ import {
|
||||
import LanguageSelector from "../common/LanguageSelector";
|
||||
import { Toaster } from "../ui/toaster";
|
||||
|
||||
const LOCALE_CHANGED_EVENT = "baron_locale_changed";
|
||||
|
||||
const navItems = [
|
||||
{
|
||||
labelKey: "ui.dev.nav.clients",
|
||||
@@ -97,10 +99,12 @@ function AppLayout() {
|
||||
const isRenewInFlightRef = useRef(false);
|
||||
const lastRenewAttemptAtRef = useRef(0);
|
||||
const lastVisitedRouteRef = useRef<string | null>(null);
|
||||
const isDevelopmentRuntime = import.meta.env.MODE === "development";
|
||||
const [theme, setTheme] = useState<"light" | "dark">(readShellTheme);
|
||||
const [isProfileMenuOpen, setIsProfileMenuOpen] = useState(false);
|
||||
const [isSessionExpiryEnabled, setIsSessionExpiryEnabled] = useState(
|
||||
readShellSessionExpiryEnabled,
|
||||
const [, setDevelopmentRenderRevision] = useState(0);
|
||||
const [isSessionExpiryEnabled, setIsSessionExpiryEnabled] = useState(() =>
|
||||
readShellSessionExpiryEnabled(!isDevelopmentRuntime),
|
||||
);
|
||||
const hasAccessToken = Boolean(auth.user?.access_token);
|
||||
const { data: profile } = useQuery({
|
||||
@@ -120,6 +124,22 @@ function AppLayout() {
|
||||
applyShellTheme(theme);
|
||||
}, [theme]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDevelopmentRuntime) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rerenderDevelopmentShell = () => {
|
||||
setDevelopmentRenderRevision((value) => value + 1);
|
||||
};
|
||||
|
||||
window.addEventListener(LOCALE_CHANGED_EVENT, rerenderDevelopmentShell);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener(LOCALE_CHANGED_EVENT, rerenderDevelopmentShell);
|
||||
};
|
||||
}, [isDevelopmentRuntime]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
@@ -185,6 +205,10 @@ function AppLayout() {
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isDevelopmentRuntime) {
|
||||
return;
|
||||
}
|
||||
|
||||
const maybeKeepSessionAlive = async () => {
|
||||
const now = Date.now();
|
||||
if (
|
||||
@@ -227,6 +251,7 @@ function AppLayout() {
|
||||
auth.isAuthenticated,
|
||||
auth.isLoading,
|
||||
auth.user?.expires_at,
|
||||
isDevelopmentRuntime,
|
||||
isSessionExpiryEnabled,
|
||||
]);
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useAuth } from "react-oidc-context";
|
||||
import { Navigate, Outlet, useLocation } from "react-router-dom";
|
||||
import { Navigate, Outlet, useLocation, useNavigate } from "react-router-dom";
|
||||
import { t } from "../../lib/i18n";
|
||||
|
||||
export default function AuthGuard() {
|
||||
const auth = useAuth();
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
const shareToken = searchParams.get("token");
|
||||
const isPlaywrightBypass =
|
||||
@@ -57,7 +58,7 @@ export default function AuthGuard() {
|
||||
className="inline-flex items-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90"
|
||||
onClick={() => {
|
||||
auth.removeUser();
|
||||
window.location.href = "/login";
|
||||
navigate("/login");
|
||||
}}
|
||||
>
|
||||
{t("ui.common.back_to_login", "로그인으로 돌아가기")}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import axios from "axios";
|
||||
import { shouldStartLoginRedirect } from "../../../common/core/auth";
|
||||
import {
|
||||
shouldSuppressDevelopmentSessionRedirect,
|
||||
} from "../../../common/core/session";
|
||||
import { userManager } from "./auth";
|
||||
|
||||
let isRedirectingToLogin = false;
|
||||
@@ -42,8 +45,22 @@ apiClient.interceptors.response.use(
|
||||
message.includes("invalid session") ||
|
||||
message.includes("token is not active")));
|
||||
|
||||
if (!shouldRedirectToLogin) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
if (
|
||||
shouldSuppressDevelopmentSessionRedirect({
|
||||
appMode: import.meta.env.MODE,
|
||||
})
|
||||
) {
|
||||
console.warn(
|
||||
"[apiClient] Auth failure detected, but development session redirects are disabled.",
|
||||
);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
if (
|
||||
shouldRedirectToLogin &&
|
||||
shouldStartLoginRedirect({
|
||||
pathname: window.location.pathname,
|
||||
isRedirecting: isRedirectingToLogin,
|
||||
|
||||
Reference in New Issue
Block a user