1
0
forked from baron/baron-sso

Merge pull request 'feature/devfront-logout' (#290) from feature/devfront-logout into dev

Reviewed-on: baron/baron-sso#290
This commit is contained in:
2026-02-23 09:56:08 +09:00
12 changed files with 163 additions and 104 deletions

View File

@@ -1,7 +1,7 @@
import { BadgeCheck, LogOut, Moon, ShieldHalf, Sun } from "lucide-react";
import { useEffect, useState } from "react";
import { useAuth } from "react-oidc-context";
import { NavLink, Outlet } from "react-router-dom";
import { NavLink, Outlet, useNavigate } from "react-router-dom";
import { t } from "../../lib/i18n";
import LanguageSelector from "../common/LanguageSelector";
import { Toaster } from "../ui/toaster";
@@ -16,11 +16,20 @@ const navItems = [
];
function AppLayout() {
const auth = useAuth();
const navigate = useNavigate();
const [theme, setTheme] = useState<"light" | "dark">(() => {
const stored = window.localStorage.getItem("admin_theme");
return stored === "dark" ? "dark" : "light";
});
const handleLogout = () => {
if (window.confirm(t("msg.dev.logout_confirm", "로그아웃 하시겠습니까?"))) {
auth.removeUser();
navigate("/login");
}
};
useEffect(() => {
const root = document.documentElement;
root.classList.remove("light", "dark");
@@ -38,60 +47,73 @@ function AppLayout() {
return (
<div className="grid min-h-screen bg-background text-foreground md:grid-cols-[240px,1fr]">
<aside className="border-b border-border bg-card md:sticky md:top-0 md:h-screen md:border-b-0 md:border-r md:bg-card md:backdrop-blur">
<div className="flex items-center justify-between px-5 py-4 md:block md:space-y-6 md:py-6">
<div className="flex items-center gap-3 md:flex-col md:items-start">
<div className="grid h-11 w-11 place-items-center rounded-xl bg-primary/15 text-primary shadow-[0_12px_30px_rgba(54,211,153,0.22)]">
<ShieldHalf size={20} />
<aside className="border-b border-border bg-card md:sticky md:top-0 md:h-screen md:border-b-0 md:border-r md:bg-card md:backdrop-blur flex flex-col justify-between">
<div>
<div className="flex items-center justify-between px-5 py-4 md:block md:space-y-6 md:py-6">
<div className="flex items-center gap-3 md:flex-col md:items-start">
<div className="grid h-11 w-11 place-items-center rounded-xl bg-primary/15 text-primary shadow-[0_12px_30px_rgba(54,211,153,0.22)]">
<ShieldHalf size={20} />
</div>
<div>
<p className="text-xs uppercase tracking-[0.18em] text-muted-foreground">
{t("ui.dev.brand", "Baron 로그인")}
</p>
<h1 className="text-lg font-semibold">
{t("ui.dev.console_title", "Developer Console")}
</h1>
</div>
</div>
<div>
<p className="text-xs uppercase tracking-[0.18em] text-muted-foreground">
{t("ui.dev.brand", "Baron 로그인")}
</p>
<h1 className="text-lg font-semibold">
{t("ui.dev.console_title", "Developer Console")}
</h1>
<div className="hidden rounded-full border border-border px-3 py-2 text-xs text-muted-foreground md:inline-flex md:items-center md:gap-2">
<BadgeCheck size={14} />
{t("ui.dev.scope_badge", "Scoped to /dev")}
</div>
</div>
<div className="hidden rounded-full border border-border px-3 py-2 text-xs text-muted-foreground md:inline-flex md:items-center md:gap-2">
<BadgeCheck size={14} />
{t("ui.dev.scope_badge", "Scoped to /dev")}
<nav className="px-2 pb-4 md:px-3 md:pb-8">
<div className="flex flex-wrap gap-2 px-3 pb-4 text-[11px] text-muted-foreground md:flex-col md:items-start">
<span className="rounded-full border border-border px-3 py-1">
{t("ui.dev.env_badge", "Env: dev")}
</span>
</div>
<div className="flex flex-col gap-1">
{navItems.map(({ labelKey, labelFallback, to, icon: Icon }) => (
<NavLink
key={to}
to={to}
className={({ isActive }) =>
[
"flex items-center gap-3 rounded-xl px-3 py-3 text-sm transition",
isActive
? "bg-primary/10 text-primary shadow-[0_12px_40px_rgba(54,211,153,0.18)]"
: "text-muted-foreground hover:bg-muted/10 hover:text-foreground",
].join(" ")
}
>
<Icon size={18} />
<span>{t(labelKey, labelFallback)}</span>
</NavLink>
))}
</div>
</nav>
<div className="hidden space-y-2 px-5 pb-6 text-xs text-[var(--color-muted)] md:block">
<p>{t("msg.dev.sidebar.notice", "개발자 전용 콘솔입니다.")}</p>
<p>
{t(
"msg.dev.sidebar.notice_detail",
"클라이언트 애플리케이션 등록 및 관리를 수행할 수 있습니다.",
)}
</p>
</div>
</div>
<nav className="px-2 pb-4 md:px-3 md:pb-8">
<div className="flex flex-wrap gap-2 px-3 pb-4 text-[11px] text-muted-foreground md:flex-col md:items-start">
<span className="rounded-full border border-border px-3 py-1">
{t("ui.dev.env_badge", "Env: dev")}
</span>
</div>
<div className="flex flex-col gap-1">
{navItems.map(({ labelKey, labelFallback, to, icon: Icon }) => (
<NavLink
key={to}
to={to}
className={({ isActive }) =>
[
"flex items-center gap-3 rounded-xl px-3 py-3 text-sm transition",
isActive
? "bg-primary/10 text-primary shadow-[0_12px_40px_rgba(54,211,153,0.18)]"
: "text-muted-foreground hover:bg-muted/10 hover:text-foreground",
].join(" ")
}
>
<Icon size={18} />
<span>{t(labelKey, labelFallback)}</span>
</NavLink>
))}
</div>
</nav>
<div className="hidden space-y-2 px-5 pb-6 text-xs text-[var(--color-muted)] md:block">
<p>{t("msg.dev.sidebar.notice", "개발자 전용 콘솔입니다.")}</p>
<p>
{t(
"msg.dev.sidebar.notice_detail",
"클라이언트 애플리케이션 등록 및 관리를 수행할 수 있습니다.",
)}
</p>
<div className="px-2 pb-6 md:px-3">
<button
type="button"
onClick={handleLogout}
className="flex w-full items-center gap-3 rounded-xl px-3 py-3 text-sm text-muted-foreground transition hover:bg-muted/10 hover:text-foreground"
>
<LogOut size={18} />
<span>{t("ui.dev.nav.logout", "Logout")}</span>
</button>
</div>
</aside>

View File

@@ -69,7 +69,7 @@ function ClientConsentsPage() {
</Link>
<span>/</span>
<Link to="/clients" className="hover:text-primary">
{t("ui.dev.clients.consents.breadcrumb.clients", "Clients")}
{t("ui.dev.clients.consents.breadcrumb.clients", "Apps")}
</Link>
<span>/</span>
<span>{clientData?.client?.name || clientId}</span>

View File

@@ -124,7 +124,7 @@ function ClientDetailsPage() {
if (isLoading) {
return (
<div className="p-8 text-center">
{t("msg.dev.clients.details.loading", "Loading client...")}
{t("msg.dev.clients.details.loading", "Loading app...")}
</div>
);
}
@@ -137,7 +137,7 @@ function ClientDetailsPage() {
<div className="p-8 text-center text-red-500">
{t(
"msg.dev.clients.details.load_error",
"Error loading client: {{error}}",
"Error loading app: {{error}}",
{ error: errMsg || t("msg.common.unknown_error", "unknown error") },
)}
</div>
@@ -185,7 +185,7 @@ function ClientDetailsPage() {
<div className="space-y-3">
<div className="flex flex-wrap items-center gap-2 text-sm text-muted-foreground">
<Link to="/clients" className="text-primary hover:underline">
{t("ui.dev.clients.details.breadcrumb.section", "Relying Parties")}
{t("ui.dev.clients.details.breadcrumb.section", "Apps")}
</Link>
<span>/</span>
<span className="text-foreground">

View File

@@ -376,7 +376,6 @@ function ClientGeneralPage() {
<table className="w-full text-sm">
<thead className="bg-muted/50 border-b border-border text-xs uppercase tracking-wider text-muted-foreground">
<tr>
<th className="px-4 py-3 text-left font-bold">Scope Name</th>
<th className="px-4 py-3 text-left font-bold">
{t(
"ui.dev.clients.general.scopes.table.name",
@@ -395,7 +394,9 @@ function ClientGeneralPage() {
"Mandatory",
)}
</th>
<th className="px-4 py-3 text-right" />
<th className="px-4 py-3 text-right font-bold">
{t("ui.dev.clients.general.scopes.table.delete", "Delete")}
</th>
</tr>
</thead>
<tbody className="divide-y divide-border">

View File

@@ -101,7 +101,7 @@ function ClientsPage() {
const stats: StatItem[] = [
{
labelKey: "ui.dev.clients.stats.total",
labelFallback: "총 클라이언트",
labelFallback: "총 애플리케이션",
value: totalClients.toString(),
deltaKey: "ui.dev.clients.stats.realtime",
deltaFallback: "Realtime",

View File

@@ -205,14 +205,15 @@ saving = "Saving..."
unknown_error = "unknown error"
[msg.dev]
logout_confirm = "Are you sure you want to log out?"
[msg.dev.clients]
copy_client_id = "Copy Client Id"
load_error = "Error loading clients: {{error}}"
loading = "Loading clients..."
showing = "Showing {{shown}} of {{total}} clients"
loading = "Loading apps..."
showing = "Showing {{shown}} of {{total}} apps"
status_update_error = "Failed to update client status"
status_updated = "Status Updated"
status_updated = "The app has been {{status}}."
[msg.dev.clients.consents]
empty = "No consents found."
@@ -927,10 +928,14 @@ console_title = "Developer Console"
env_badge = "Env: dev"
scope_badge = "Scoped to /dev"
[ui.dev.nav]
clients = "Connected Application"
logout = "Logout"
[ui.dev.clients]
copy_client_id = "Copy client id"
new = "New"
search_placeholder = "Search Placeholder"
new = "Add Connected Application"
search_placeholder = "Search by app name or ID..."
tenant_scoped = "Tenant-scoped"
untitled = "Untitled"
@@ -1006,8 +1011,8 @@ consents = "Consent & Users"
settings = "Settings"
[ui.dev.clients.general]
create = "Create"
display_new = "Display New"
create = "Create Application"
display_new = "Add Connected Application"
save = "Settings Save"
title_create = "Create Client"
title_edit = "Client Settings"
@@ -1043,6 +1048,7 @@ title = "Scopes"
description = "Description"
mandatory = "Mandatory"
name = "Scope Name"
delete = "Delete"
[ui.dev.clients.general.security]
confidential = "Confidential"
@@ -1055,7 +1061,7 @@ title = "Need help with OIDC configuration?"
view_guides = "View guides"
[ui.dev.clients.list]
title = "Title"
title = "Connected Applications"
[ui.dev.clients.owner]
avatar_alt = "ops user"

View File

@@ -205,14 +205,15 @@ saving = "저장 중..."
unknown_error = "unknown error"
[msg.dev]
logout_confirm = "로그아웃 하시겠습니까?"
[msg.dev.clients]
copy_client_id = "클라이언트 ID가 복사되었습니다."
copy_client_id = "Client ID가 복사되었습니다."
load_error = "Error loading clients: {{error}}"
loading = "Loading clients..."
showing = "Showing {{shown}} of {{total}} clients"
loading = "Loading apps..."
showing = "Showing {{shown}} of {{total}} apps"
status_update_error = "Failed to update client status"
status_updated = "클라이언트가 {{status}}되었습니다."
status_updated = "앱이 {{status}}되었습니다."
[msg.dev.clients.consents]
empty = "No consents found."
@@ -257,19 +258,19 @@ help = "인증 후 리다이렉트될 URI를 입력하세요. 생성 후 Connect
[msg.dev.clients.general.scopes]
empty = "등록된 스코프가 없습니다."
subtitle = "이 클라이언트가 요청할 수 있는 권한 범위를 정의합니다."
subtitle = "이 앱이 요청할 수 있는 권한 범위를 정의합니다."
[msg.dev.clients.general.security]
confidential_help = "서버 사이드 앱(예: Node.js, Java)처럼 비밀키를 안전하게 보관 가능한 경우."
public_help = "SPA/모바일 앱처럼 비밀키 보관이 어려운 경우. PKCE를 기본 사용합니다."
subtitle = "클라이언트 유형을 선택하세요. 보안 수준에 따라 인증 방식이 달라집니다."
subtitle = " 유형을 선택하세요. 보안 수준에 따라 인증 방식이 달라집니다."
[msg.dev.clients.help]
docs_body = "Includes PKCE, client_secret_basic, redirect URI validation tips."
subtitle = "Developer guides for Confidential/Public clients, redirect URIs, and auth methods."
[msg.dev.clients.registry]
description = "OIDC 클라이언트, 인증 방식, 리다이렉트 URI, 비밀키 재발행을 감사 로그와 함께 관리합니다."
description = "OIDC , 인증 방식, 리다이렉트 URI, 비밀키 재발행을 감사 로그와 함께 관리합니다."
[msg.dev.clients.scopes]
email = "이메일 주소 접근"
@@ -291,7 +292,7 @@ hydra_health = "Hydra Admin 상태 체크 준비"
[msg.dev.sidebar]
notice = "개발자 전용 콘솔입니다."
notice_detail = "클라이언트 애플리케이션 등록 및 관리를 수행할 수 있습니다."
notice_detail = "연동 앱 등록 및 관리를 수행할 수 있습니다."
[msg.info]
saved_success = "저장이 완료되었습니다."
@@ -927,10 +928,14 @@ console_title = "Developer Console"
env_badge = "Env: dev"
scope_badge = "Scoped to /dev"
[ui.dev.nav]
clients = "연동 앱"
logout = "로그아웃"
[ui.dev.clients]
copy_client_id = "Copy client id"
new = "새 클라이언트"
search_placeholder = "클라이언트 이름/ID로 검색..."
new = "연동 앱 추가"
search_placeholder = "연동 앱 이름/ID로 검색..."
tenant_scoped = "Tenant-scoped"
untitled = "Untitled"
@@ -973,13 +978,13 @@ user = "User"
[ui.dev.clients.details]
[ui.dev.clients.details.breadcrumb]
current = "클라이언트 상세"
current = "연동 앱 상세"
section = "Relying Parties"
[ui.dev.clients.details.credentials]
client_id = "Client ID"
client_secret = "Client Secret"
title = "클라이언트 자격 증명"
title = " 자격 증명"
[ui.dev.clients.details.endpoints]
read_only = "읽기 전용"
@@ -1006,8 +1011,8 @@ consents = "Consent & Users"
settings = "Settings"
[ui.dev.clients.general]
create = "클라이언트 생성"
display_new = "새 클라이언트"
create = " 생성"
display_new = "연동 앱 추가"
save = "설정 저장"
title_create = "Create Client"
title_edit = "Client Settings"
@@ -1043,6 +1048,7 @@ title = "Scopes"
description = "Description"
mandatory = "Mandatory"
name = "Scope Name"
delete = "Delete"
[ui.dev.clients.general.security]
confidential = "Confidential"
@@ -1055,7 +1061,7 @@ title = "Need help with OIDC configuration?"
view_guides = "View guides"
[ui.dev.clients.list]
title = "클라이언트 목록"
title = "연동 앱 목록"
[ui.dev.clients.owner]
avatar_alt = "ops user"

View File

@@ -205,6 +205,7 @@ saving = ""
unknown_error = ""
[msg.dev]
logout_confirm = ""
[msg.dev.clients]
copy_client_id = ""
@@ -939,6 +940,10 @@ console_title = ""
env_badge = ""
scope_badge = ""
[ui.dev.nav]
clients = ""
logout = ""
[ui.dev.clients]
copy_client_id = ""
new = ""
@@ -1055,6 +1060,7 @@ title = ""
description = ""
mandatory = ""
name = ""
delete = ""
[ui.dev.clients.general.security]
confidential = ""

View File

@@ -68,7 +68,7 @@ test("clients page loads correctly", async ({ page }) => {
await expect(page).toHaveTitle(/바론 개발자 서비스/);
// 페이지 내 주요 텍스트 확인
await expect(page.getByText("클라이언트 목록")).toBeVisible();
await expect(page.getByText("연동 앱 목록")).toBeVisible();
// 테이블 헤더 확인
await expect(

View File

@@ -205,14 +205,15 @@ saving = "Saving..."
unknown_error = "unknown error"
[msg.dev]
logout_confirm = "Are you sure you want to log out?"
[msg.dev.clients]
copy_client_id = "Copy Client Id"
load_error = "Error loading clients: {{error}}"
loading = "Loading clients..."
showing = "Showing {{shown}} of {{total}} clients"
loading = "Loading apps..."
showing = "Showing {{shown}} of {{total}} apps"
status_update_error = "Failed to update client status"
status_updated = "Status Updated"
status_updated = "The app has been {{status}}."
[msg.dev.clients.consents]
empty = "No consents found."
@@ -927,10 +928,14 @@ console_title = "Developer Console"
env_badge = "Env: dev"
scope_badge = "Scoped to /dev"
[ui.dev.nav]
clients = "Connected Application"
logout = "Logout"
[ui.dev.clients]
copy_client_id = "Copy client id"
new = "New"
search_placeholder = "Search Placeholder"
new = "Add Connected Application"
search_placeholder = "Search by app name or ID..."
tenant_scoped = "Tenant-scoped"
untitled = "Untitled"
@@ -1006,8 +1011,8 @@ consents = "Consent & Users"
settings = "Settings"
[ui.dev.clients.general]
create = "Create"
display_new = "Display New"
create = "Create Application"
display_new = "Add Connected Application"
save = "Settings Save"
title_create = "Create Client"
title_edit = "Client Settings"
@@ -1043,6 +1048,7 @@ title = "Scopes"
description = "Description"
mandatory = "Mandatory"
name = "Scope Name"
delete = "Delete"
[ui.dev.clients.general.security]
confidential = "Confidential"
@@ -1055,7 +1061,7 @@ title = "Need help with OIDC configuration?"
view_guides = "View guides"
[ui.dev.clients.list]
title = "Title"
title = "Connected Applications"
[ui.dev.clients.owner]
avatar_alt = "ops user"

View File

@@ -205,14 +205,15 @@ saving = "저장 중..."
unknown_error = "unknown error"
[msg.dev]
logout_confirm = "로그아웃 하시겠습니까?"
[msg.dev.clients]
copy_client_id = "클라이언트 ID가 복사되었습니다."
copy_client_id = "Client ID가 복사되었습니다."
load_error = "Error loading clients: {{error}}"
loading = "Loading clients..."
showing = "Showing {{shown}} of {{total}} clients"
loading = "Loading apps..."
showing = "Showing {{shown}} of {{total}} apps"
status_update_error = "Failed to update client status"
status_updated = "클라이언트가 {{status}}되었습니다."
status_updated = "앱이 {{status}}되었습니다."
[msg.dev.clients.consents]
empty = "No consents found."
@@ -257,19 +258,19 @@ help = "인증 후 리다이렉트될 URI를 입력하세요. 생성 후 Connect
[msg.dev.clients.general.scopes]
empty = "등록된 스코프가 없습니다."
subtitle = "이 클라이언트가 요청할 수 있는 권한 범위를 정의합니다."
subtitle = "이 앱이 요청할 수 있는 권한 범위를 정의합니다."
[msg.dev.clients.general.security]
confidential_help = "서버 사이드 앱(예: Node.js, Java)처럼 비밀키를 안전하게 보관 가능한 경우."
public_help = "SPA/모바일 앱처럼 비밀키 보관이 어려운 경우. PKCE를 기본 사용합니다."
subtitle = "클라이언트 유형을 선택하세요. 보안 수준에 따라 인증 방식이 달라집니다."
subtitle = " 유형을 선택하세요. 보안 수준에 따라 인증 방식이 달라집니다."
[msg.dev.clients.help]
docs_body = "Includes PKCE, client_secret_basic, redirect URI validation tips."
subtitle = "Developer guides for Confidential/Public clients, redirect URIs, and auth methods."
[msg.dev.clients.registry]
description = "OIDC 클라이언트, 인증 방식, 리다이렉트 URI, 비밀키 재발행을 감사 로그와 함께 관리합니다."
description = "OIDC , 인증 방식, 리다이렉트 URI, 비밀키 재발행을 감사 로그와 함께 관리합니다."
[msg.dev.clients.scopes]
email = "이메일 주소 접근"
@@ -291,7 +292,7 @@ hydra_health = "Hydra Admin 상태 체크 준비"
[msg.dev.sidebar]
notice = "개발자 전용 콘솔입니다."
notice_detail = "클라이언트 애플리케이션 등록 및 관리를 수행할 수 있습니다."
notice_detail = "연동 앱 등록 및 관리를 수행할 수 있습니다."
[msg.info]
saved_success = "저장이 완료되었습니다."
@@ -927,10 +928,14 @@ console_title = "Developer Console"
env_badge = "Env: dev"
scope_badge = "Scoped to /dev"
[ui.dev.nav]
clients = "연동 앱"
logout = "로그아웃"
[ui.dev.clients]
copy_client_id = "Copy client id"
new = "새 클라이언트"
search_placeholder = "클라이언트 이름/ID로 검색..."
new = "연동 앱 추가"
search_placeholder = "연동 앱 이름/ID로 검색..."
tenant_scoped = "Tenant-scoped"
untitled = "Untitled"
@@ -973,13 +978,13 @@ user = "User"
[ui.dev.clients.details]
[ui.dev.clients.details.breadcrumb]
current = "클라이언트 상세"
current = "연동 앱 상세"
section = "Relying Parties"
[ui.dev.clients.details.credentials]
client_id = "Client ID"
client_secret = "Client Secret"
title = "클라이언트 자격 증명"
title = " 자격 증명"
[ui.dev.clients.details.endpoints]
read_only = "읽기 전용"
@@ -1006,8 +1011,8 @@ consents = "Consent & Users"
settings = "Settings"
[ui.dev.clients.general]
create = "클라이언트 생성"
display_new = "새 클라이언트"
create = " 생성"
display_new = "연동 앱 추가"
save = "설정 저장"
title_create = "Create Client"
title_edit = "Client Settings"
@@ -1043,6 +1048,7 @@ title = "Scopes"
description = "Description"
mandatory = "Mandatory"
name = "Scope Name"
delete = "Delete"
[ui.dev.clients.general.security]
confidential = "Confidential"
@@ -1055,7 +1061,7 @@ title = "Need help with OIDC configuration?"
view_guides = "View guides"
[ui.dev.clients.list]
title = "클라이언트 목록"
title = "연동 앱 목록"
[ui.dev.clients.owner]
avatar_alt = "ops user"

View File

@@ -205,6 +205,7 @@ saving = ""
unknown_error = ""
[msg.dev]
logout_confirm = ""
[msg.dev.clients]
copy_client_id = ""
@@ -939,6 +940,10 @@ console_title = ""
env_badge = ""
scope_badge = ""
[ui.dev.nav]
clients = ""
logout = ""
[ui.dev.clients]
copy_client_id = ""
new = ""
@@ -1055,6 +1060,7 @@ title = ""
description = ""
mandatory = ""
name = ""
delete = ""
[ui.dev.clients.general.security]
confidential = ""