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