1
0
forked from baron/baron-sso

devfront 설정 탭 섹션 순서 조정

This commit is contained in:
2026-06-17 16:00:14 +09:00
parent c308d0a7d4
commit 28dad91b1a

View File

@@ -1892,85 +1892,8 @@ function ClientGeneralPage() {
</div>
</div>
<Card className="glass-panel">
<CardHeader className="pb-4">
<div className="flex flex-col gap-2 md:flex-row md:items-start md:justify-between">
<div className="space-y-1">
<CardTitle className="text-xl font-bold">
{t("ui.dev.clients.general.auto_login.title", "자동 로그인")}
</CardTitle>
<CardDescription>
{t(
"msg.dev.clients.general.auto_login.subtitle",
"RP가 자체 로그인 시작 URL에서 OIDC 요청을 만들 수 있으면 userfront에서 바로 로그인 진입을 제공합니다.",
)}
</CardDescription>
</div>
<div className="flex items-center gap-3 rounded-xl border border-border bg-muted/30 px-4 py-3">
<div className="space-y-0.5 text-right">
<p className="text-sm font-semibold">
{autoLoginSupported
? t("ui.common.enabled", "사용")
: t("ui.common.disabled", "사용 안 함")}
</p>
<p className="text-xs text-muted-foreground">
{t(
"ui.dev.clients.general.auto_login.supported",
"자동 로그인 지원",
)}
</p>
</div>
<Switch
checked={autoLoginSupported}
onCheckedChange={setAutoLoginSupported}
id="auto-login-supported"
aria-label={t(
"ui.dev.clients.general.auto_login.supported",
"자동 로그인 지원",
)}
/>
</div>
</div>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="auto-login-url" className="text-sm font-semibold">
{t(
"ui.dev.clients.general.auto_login.url",
"자동 로그인 시작 URL",
)}
</Label>
<Input
id="auto-login-url"
value={autoLoginUrl}
onChange={(event) => setAutoLoginUrl(event.target.value)}
disabled={!autoLoginSupported}
aria-invalid={!hasValidAutoLoginUrl}
className={!hasValidAutoLoginUrl ? "border-destructive" : ""}
placeholder={t(
"ui.dev.clients.general.auto_login.url_placeholder",
"https://app.example.com/login?auto=1",
)}
/>
<p className="text-xs text-muted-foreground">
{t(
"msg.dev.clients.general.auto_login.help",
"이 URL은 RP가 state, nonce, PKCE 값을 직접 생성한 뒤 Baron OIDC로 리다이렉트해야 합니다.",
)}
</p>
{!hasValidAutoLoginUrl ? (
<p className="text-xs text-destructive">
{t(
"msg.dev.clients.general.auto_login.invalid_url",
"자동 로그인 URL 형식이 올바르지 않습니다. http 또는 https 주소를 입력하세요.",
)}
</p>
) : null}
</div>
</CardContent>
</Card>
{/* 2. Scopes */}
{/* 3. Custom Claims */}
<Card className="glass-panel">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-4">
<div>
@@ -2389,229 +2312,6 @@ function ClientGeneralPage() {
</CardContent>
</Card>
<Card className="glass-panel">
<CardHeader className="pb-4">
<div className="flex flex-col gap-2 md:flex-row md:items-start md:justify-between">
<div className="space-y-1">
<CardTitle className="text-xl font-bold">
{t(
"ui.dev.clients.general.tenant_access.title",
"테넌트 접근 제한",
)}
</CardTitle>
<div className="text-sm text-muted-foreground">
<p className="leading-relaxed">
{t(
"ui.dev.clients.general.tenant_access.subtitle",
"허용된 테넌트만 이 RP에 접근할 수 있도록 제한합니다.",
)}
</p>
<p className="leading-relaxed">
{t(
"ui.dev.clients.general.tenant_access.hint",
"제한을 켜면 tenants 스코프가 자동으로 포함되며, 허용 테넌트를 하나 이상 선택해야 합니다.",
)}
</p>
</div>
</div>
<div className="flex items-center gap-3 rounded-xl border border-border bg-muted/30 px-4 py-3">
<div className="space-y-0.5 text-right">
<p className="text-sm font-semibold">
{tenantAccessRestricted
? t(
"ui.dev.clients.general.tenant_access.enabled",
"제한 있음",
)
: t(
"ui.dev.clients.general.tenant_access.disabled",
"제한 없음",
)}
</p>
<p className="text-xs text-muted-foreground">
{t(
"ui.dev.clients.general.tenant_access.title",
"테넌트 접근 제한",
)}
</p>
</div>
<Switch
checked={tenantAccessRestricted}
onCheckedChange={handleTenantAccessToggle}
id="tenant-access-toggle"
disabled={isGeneralSettingsReadOnly}
/>
</div>
</div>
</CardHeader>
<CardContent className="space-y-3">
{tenantAccessRestricted ? (
<div className="grid gap-4 lg:grid-cols-[0.8fr_1.2fr]">
<div className="space-y-3">
<Label className="text-sm font-semibold">
{t(
"ui.dev.clients.general.tenant_access.picker_label",
"Add allowed tenant",
)}{" "}
<span className="text-destructive">*</span>
</Label>
<TenantAccessPicker
disabled={isGeneralSettingsReadOnly}
selectedCount={allowedTenantIds.length}
onSelectTenant={(selection) =>
handleSelectAllowedTenant(selection.id)
}
/>
</div>
<div className="space-y-3">
<Label className="text-sm font-semibold">
{t(
"ui.dev.clients.general.tenant_access.selected_title",
"허용 테넌트",
)}
</Label>
<SettingsTableShell bodyClassName="max-h-80">
<SettingsTable>
<SettingsTableHeader className="sticky top-0 z-10">
<tr>
<SettingsTableHead className="w-[28%]">
{t(
"ui.dev.clients.general.tenant_access.table.name",
"테넌트명",
)}
</SettingsTableHead>
<SettingsTableHead className="w-[18%]">
{t(
"ui.dev.clients.general.tenant_access.table.slug",
"슬러그",
)}
</SettingsTableHead>
<SettingsTableHead>
{t(
"ui.dev.clients.general.tenant_access.table.id",
"테넌트 ID",
)}
</SettingsTableHead>
<SettingsTableHead className="w-[112px] text-right">
{t(
"ui.dev.clients.general.tenant_access.table.actions",
"작업",
)}
</SettingsTableHead>
</tr>
</SettingsTableHeader>
<SettingsTableBody>
{selectedAllowedTenants.length > 0 ? (
<>
{selectedAllowedTenants.map((tenant) => (
<SettingsTableRow
key={tenant.id}
data-testid={`allowed-tenant-${tenant.id}`}
>
<SettingsTableCell className="align-middle font-medium">
<span className="block truncate">
{tenant.name}
</span>
</SettingsTableCell>
<SettingsTableCell className="align-middle text-muted-foreground">
{tenant.slug || "-"}
</SettingsTableCell>
<SettingsTableCell className="align-middle font-mono text-sm text-muted-foreground">
<span className="break-all">{tenant.id}</span>
</SettingsTableCell>
<SettingsTableCell className="align-middle text-right">
<div className="inline-flex items-center gap-1.5">
<CopyButton
aria-label="테넌트 UUID 복사"
className="h-8 w-8"
data-testid={`allowed-tenant-copy-${tenant.id}`}
size="icon"
value={tenant.id}
variant="ghost"
/>
<button
type="button"
aria-label={t("ui.common.delete", "삭제")}
onClick={() =>
toggleAllowedTenant(tenant.id)
}
className="inline-flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground transition hover:text-destructive"
data-testid={`allowed-tenant-remove-${tenant.id}`}
disabled={isGeneralSettingsReadOnly}
>
<Trash2 className="h-4 w-4" />
</button>
</div>
</SettingsTableCell>
</SettingsTableRow>
))}
{allowedTenantIds
.filter(
(tenantId) =>
!selectedAllowedTenants.some(
(tenant) => tenant.id === tenantId,
),
)
.map((tenantId) => (
<SettingsTableRow
key={tenantId}
data-testid={`allowed-tenant-${tenantId}`}
>
<SettingsTableCell className="align-middle font-medium">
<span className="block truncate">
{tenantId}
</span>
</SettingsTableCell>
<SettingsTableCell className="align-middle text-muted-foreground">
-
</SettingsTableCell>
<SettingsTableCell className="align-middle font-mono text-sm text-muted-foreground">
<span className="break-all">{tenantId}</span>
</SettingsTableCell>
<SettingsTableCell className="align-middle text-right">
<div className="inline-flex items-center gap-1.5">
<CopyButton
aria-label="테넌트 UUID 복사"
className="h-8 w-8"
data-testid={`allowed-tenant-copy-${tenantId}`}
size="icon"
value={tenantId}
variant="ghost"
/>
<button
type="button"
aria-label={t("ui.common.delete", "삭제")}
onClick={() =>
toggleAllowedTenant(tenantId)
}
className="inline-flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground transition hover:text-destructive"
data-testid={`allowed-tenant-remove-${tenantId}`}
disabled={isGeneralSettingsReadOnly}
>
<Trash2 className="h-4 w-4" />
</button>
</div>
</SettingsTableCell>
</SettingsTableRow>
))}
</>
) : (
<SettingsTableEmptyState colSpan={4} className="py-4">
{t(
"ui.dev.clients.general.tenant_access.selected_empty",
"아직 선택된 테넌트가 없습니다.",
)}
</SettingsTableEmptyState>
)}
</SettingsTableBody>
</SettingsTable>
</SettingsTableShell>
</div>
</div>
) : null}
</CardContent>
</Card>
<Card className="glass-panel">
<CardHeader className="pb-4">
<div className="flex flex-col gap-2 md:flex-row md:items-start md:justify-between">
@@ -3029,7 +2729,310 @@ function ClientGeneralPage() {
</CardContent>
</Card>
{/* 3. Security Settings */}
{/* 4. Tenant Access Restriction */}
<Card className="glass-panel">
<CardHeader className="pb-4">
<div className="flex flex-col gap-2 md:flex-row md:items-start md:justify-between">
<div className="space-y-1">
<CardTitle className="text-xl font-bold">
{t(
"ui.dev.clients.general.tenant_access.title",
"테넌트 접근 제한",
)}
</CardTitle>
<div className="text-sm text-muted-foreground">
<p className="leading-relaxed">
{t(
"ui.dev.clients.general.tenant_access.subtitle",
"허용된 테넌트만 이 RP에 접근할 수 있도록 제한합니다.",
)}
</p>
<p className="leading-relaxed">
{t(
"ui.dev.clients.general.tenant_access.hint",
"제한을 켜면 tenants 스코프가 자동으로 포함되며, 허용 테넌트를 하나 이상 선택해야 합니다.",
)}
</p>
</div>
</div>
<div className="flex items-center gap-3 rounded-xl border border-border bg-muted/30 px-4 py-3">
<div className="space-y-0.5 text-right">
<p className="text-sm font-semibold">
{tenantAccessRestricted
? t(
"ui.dev.clients.general.tenant_access.enabled",
"제한 있음",
)
: t(
"ui.dev.clients.general.tenant_access.disabled",
"제한 없음",
)}
</p>
<p className="text-xs text-muted-foreground">
{t(
"ui.dev.clients.general.tenant_access.title",
"테넌트 접근 제한",
)}
</p>
</div>
<Switch
checked={tenantAccessRestricted}
onCheckedChange={handleTenantAccessToggle}
id="tenant-access-toggle"
disabled={isGeneralSettingsReadOnly}
/>
</div>
</div>
</CardHeader>
<CardContent className="space-y-3">
{tenantAccessRestricted ? (
<div className="grid gap-4 lg:grid-cols-[0.8fr_1.2fr]">
<div className="space-y-3">
<Label className="text-sm font-semibold">
{t(
"ui.dev.clients.general.tenant_access.picker_label",
"Add allowed tenant",
)}{" "}
<span className="text-destructive">*</span>
</Label>
<TenantAccessPicker
disabled={isGeneralSettingsReadOnly}
selectedCount={allowedTenantIds.length}
onSelectTenant={(selection) =>
handleSelectAllowedTenant(selection.id)
}
/>
</div>
<div className="space-y-3">
<Label className="text-sm font-semibold">
{t(
"ui.dev.clients.general.tenant_access.selected_title",
"허용 테넌트",
)}
</Label>
<SettingsTableShell bodyClassName="max-h-80">
<SettingsTable>
<SettingsTableHeader className="sticky top-0 z-10">
<tr>
<SettingsTableHead className="w-[28%]">
{t(
"ui.dev.clients.general.tenant_access.table.name",
"테넌트명",
)}
</SettingsTableHead>
<SettingsTableHead className="w-[18%]">
{t(
"ui.dev.clients.general.tenant_access.table.slug",
"슬러그",
)}
</SettingsTableHead>
<SettingsTableHead>
{t(
"ui.dev.clients.general.tenant_access.table.id",
"테넌트 ID",
)}
</SettingsTableHead>
<SettingsTableHead className="w-[112px] text-right">
{t(
"ui.dev.clients.general.tenant_access.table.actions",
"작업",
)}
</SettingsTableHead>
</tr>
</SettingsTableHeader>
<SettingsTableBody>
{selectedAllowedTenants.length > 0 ? (
<>
{selectedAllowedTenants.map((tenant) => (
<SettingsTableRow
key={tenant.id}
data-testid={`allowed-tenant-${tenant.id}`}
>
<SettingsTableCell className="align-middle font-medium">
<span className="block truncate">
{tenant.name}
</span>
</SettingsTableCell>
<SettingsTableCell className="align-middle text-muted-foreground">
{tenant.slug || "-"}
</SettingsTableCell>
<SettingsTableCell className="align-middle font-mono text-sm text-muted-foreground">
<span className="break-all">{tenant.id}</span>
</SettingsTableCell>
<SettingsTableCell className="align-middle text-right">
<div className="inline-flex items-center gap-1.5">
<CopyButton
aria-label="테넌트 UUID 복사"
className="h-8 w-8"
data-testid={`allowed-tenant-copy-${tenant.id}`}
size="icon"
value={tenant.id}
variant="ghost"
/>
<button
type="button"
aria-label={t("ui.common.delete", "삭제")}
onClick={() =>
toggleAllowedTenant(tenant.id)
}
className="inline-flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground transition hover:text-destructive"
data-testid={`allowed-tenant-remove-${tenant.id}`}
disabled={isGeneralSettingsReadOnly}
>
<Trash2 className="h-4 w-4" />
</button>
</div>
</SettingsTableCell>
</SettingsTableRow>
))}
{allowedTenantIds
.filter(
(tenantId) =>
!selectedAllowedTenants.some(
(tenant) => tenant.id === tenantId,
),
)
.map((tenantId) => (
<SettingsTableRow
key={tenantId}
data-testid={`allowed-tenant-${tenantId}`}
>
<SettingsTableCell className="align-middle font-medium">
<span className="block truncate">
{tenantId}
</span>
</SettingsTableCell>
<SettingsTableCell className="align-middle text-muted-foreground">
-
</SettingsTableCell>
<SettingsTableCell className="align-middle font-mono text-sm text-muted-foreground">
<span className="break-all">{tenantId}</span>
</SettingsTableCell>
<SettingsTableCell className="align-middle text-right">
<div className="inline-flex items-center gap-1.5">
<CopyButton
aria-label="테넌트 UUID 복사"
className="h-8 w-8"
data-testid={`allowed-tenant-copy-${tenantId}`}
size="icon"
value={tenantId}
variant="ghost"
/>
<button
type="button"
aria-label={t("ui.common.delete", "삭제")}
onClick={() =>
toggleAllowedTenant(tenantId)
}
className="inline-flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground transition hover:text-destructive"
data-testid={`allowed-tenant-remove-${tenantId}`}
disabled={isGeneralSettingsReadOnly}
>
<Trash2 className="h-4 w-4" />
</button>
</div>
</SettingsTableCell>
</SettingsTableRow>
))}
</>
) : (
<SettingsTableEmptyState colSpan={4} className="py-4">
{t(
"ui.dev.clients.general.tenant_access.selected_empty",
"아직 선택된 테넌트가 없습니다.",
)}
</SettingsTableEmptyState>
)}
</SettingsTableBody>
</SettingsTable>
</SettingsTableShell>
</div>
</div>
) : null}
</CardContent>
</Card>
{/* 5. Auto Login */}
<Card className="glass-panel">
<CardHeader className="pb-4">
<div className="flex flex-col gap-2 md:flex-row md:items-start md:justify-between">
<div className="space-y-1">
<CardTitle className="text-xl font-bold">
{t("ui.dev.clients.general.auto_login.title", "자동 로그인")}
</CardTitle>
<CardDescription>
{t(
"msg.dev.clients.general.auto_login.subtitle",
"RP가 자체 로그인 시작 URL에서 OIDC 요청을 만들 수 있으면 userfront에서 바로 로그인 진입을 제공합니다.",
)}
</CardDescription>
</div>
<div className="flex items-center gap-3 rounded-xl border border-border bg-muted/30 px-4 py-3">
<div className="space-y-0.5 text-right">
<p className="text-sm font-semibold">
{autoLoginSupported
? t("ui.common.enabled", "사용")
: t("ui.common.disabled", "사용 안 함")}
</p>
<p className="text-xs text-muted-foreground">
{t(
"ui.dev.clients.general.auto_login.supported",
"자동 로그인 지원",
)}
</p>
</div>
<Switch
checked={autoLoginSupported}
onCheckedChange={setAutoLoginSupported}
id="auto-login-supported"
aria-label={t(
"ui.dev.clients.general.auto_login.supported",
"자동 로그인 지원",
)}
/>
</div>
</div>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="auto-login-url" className="text-sm font-semibold">
{t(
"ui.dev.clients.general.auto_login.url",
"자동 로그인 시작 URL",
)}
</Label>
<Input
id="auto-login-url"
value={autoLoginUrl}
onChange={(event) => setAutoLoginUrl(event.target.value)}
disabled={!autoLoginSupported}
aria-invalid={!hasValidAutoLoginUrl}
className={!hasValidAutoLoginUrl ? "border-destructive" : ""}
placeholder={t(
"ui.dev.clients.general.auto_login.url_placeholder",
"https://app.example.com/login?auto=1",
)}
/>
<p className="text-xs text-muted-foreground">
{t(
"msg.dev.clients.general.auto_login.help",
"이 URL은 RP가 state, nonce, PKCE 값을 직접 생성한 뒤 Baron OIDC로 리다이렉트해야 합니다.",
)}
</p>
{!hasValidAutoLoginUrl ? (
<p className="text-xs text-destructive">
{t(
"msg.dev.clients.general.auto_login.invalid_url",
"자동 로그인 URL 형식이 올바르지 않습니다. http 또는 https 주소를 입력하세요.",
)}
</p>
) : null}
</div>
</CardContent>
</Card>
{/* 6. Security Settings */}
<Card className="glass-panel">
<CardHeader className="pb-3">
<CardTitle className="text-xl font-bold">