forked from baron/baron-sso
RP 정책 설정 UI 수정
This commit is contained in:
@@ -24,6 +24,7 @@ import {
|
||||
} from "../../components/ui/card";
|
||||
import { Input } from "../../components/ui/input";
|
||||
import { Label } from "../../components/ui/label";
|
||||
import { ScrollArea } from "../../components/ui/scroll-area";
|
||||
import { Switch } from "../../components/ui/switch";
|
||||
import { Textarea } from "../../components/ui/textarea";
|
||||
import { toast } from "../../components/ui/use-toast";
|
||||
@@ -31,6 +32,7 @@ import {
|
||||
createClient,
|
||||
deleteClient,
|
||||
fetchClient,
|
||||
fetchTenants,
|
||||
refreshHeadlessJwksCache,
|
||||
revokeHeadlessJwksCache,
|
||||
updateClient,
|
||||
@@ -40,6 +42,7 @@ import type {
|
||||
ClientStatus,
|
||||
ClientType,
|
||||
ClientUpsertRequest,
|
||||
TenantSummary,
|
||||
} from "../../lib/devApi";
|
||||
import { t } from "../../lib/i18n";
|
||||
import { cn } from "../../lib/utils";
|
||||
@@ -50,6 +53,7 @@ interface ScopeItem {
|
||||
name: string;
|
||||
description: string;
|
||||
mandatory: boolean;
|
||||
locked?: boolean;
|
||||
}
|
||||
|
||||
type SecurityProfile = "private" | "pkce";
|
||||
@@ -131,6 +135,10 @@ function ClientGeneralPage() {
|
||||
queryFn: () => fetchClient(clientId as string),
|
||||
enabled: !isCreate,
|
||||
});
|
||||
const { data: tenantData } = useQuery({
|
||||
queryKey: ["tenants"],
|
||||
queryFn: () => fetchTenants(1000, 0),
|
||||
});
|
||||
|
||||
const [name, setName] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
@@ -142,6 +150,9 @@ function ClientGeneralPage() {
|
||||
const [status, setStatus] = useState<ClientStatus>("active");
|
||||
const [initialStatus, setInitialStatus] = useState<ClientStatus>("active");
|
||||
const [redirectUris, setRedirectUris] = useState("");
|
||||
const [tenantAccessRestricted, setTenantAccessRestricted] = useState(false);
|
||||
const [allowedTenantIds, setAllowedTenantIds] = useState<string[]>([]);
|
||||
const [tenantSearch, setTenantSearch] = useState("");
|
||||
|
||||
// Public Key Registration States
|
||||
const [tokenEndpointAuthMethod, setTokenEndpointAuthMethod] =
|
||||
@@ -168,6 +179,15 @@ function ClientGeneralPage() {
|
||||
description: t("msg.dev.clients.scopes.email", "이메일 주소 접근"),
|
||||
mandatory: false,
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
name: "tenant",
|
||||
description: t(
|
||||
"msg.dev.clients.scopes.tenant",
|
||||
"소속 테넌트 정보 접근",
|
||||
),
|
||||
mandatory: false,
|
||||
},
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -185,6 +205,16 @@ function ClientGeneralPage() {
|
||||
|
||||
const headlessEnabled = !!metadata.headless_login_enabled;
|
||||
setHeadlessLoginEnabled(headlessEnabled);
|
||||
const restrictedTenants = Array.isArray(metadata.allowed_tenants)
|
||||
? metadata.allowed_tenants
|
||||
.map((value) => (typeof value === "string" ? value.trim() : ""))
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
setTenantAccessRestricted(
|
||||
restrictedTenants.length > 0 ||
|
||||
metadata.tenant_access_restricted === true,
|
||||
);
|
||||
setAllowedTenantIds(restrictedTenants);
|
||||
|
||||
const savedAuthMethod =
|
||||
client.tokenEndpointAuthMethod ||
|
||||
@@ -230,15 +260,25 @@ function ClientGeneralPage() {
|
||||
|
||||
const savedScopes = metadata.structured_scopes as ScopeItem[] | undefined;
|
||||
if (savedScopes && Array.isArray(savedScopes)) {
|
||||
setScopes(savedScopes);
|
||||
setScopes(
|
||||
normalizeScopesForTenantAccess(
|
||||
savedScopes,
|
||||
restrictedTenants.length > 0 ||
|
||||
metadata.tenant_access_restricted === true,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
setScopes(
|
||||
client.scopes.map((s, idx) => ({
|
||||
id: String(idx + 1),
|
||||
name: s,
|
||||
description: "",
|
||||
mandatory: s === "openid",
|
||||
})),
|
||||
normalizeScopesForTenantAccess(
|
||||
client.scopes.map((s, idx) => ({
|
||||
id: String(idx + 1),
|
||||
name: s,
|
||||
description: "",
|
||||
mandatory: s === "openid",
|
||||
})),
|
||||
restrictedTenants.length > 0 ||
|
||||
metadata.tenant_access_restricted === true,
|
||||
),
|
||||
);
|
||||
}
|
||||
}, [data]);
|
||||
@@ -279,6 +319,55 @@ function ClientGeneralPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const tenantScopeDescription = t(
|
||||
"msg.dev.clients.scopes.tenant",
|
||||
"소속 테넌트 정보 접근",
|
||||
);
|
||||
|
||||
const buildTenantScope = (id: string): ScopeItem => ({
|
||||
id,
|
||||
name: "tenant",
|
||||
description: tenantScopeDescription,
|
||||
mandatory: true,
|
||||
locked: true,
|
||||
});
|
||||
|
||||
function normalizeScopesForTenantAccess(
|
||||
nextScopes: ScopeItem[],
|
||||
restricted: boolean,
|
||||
): ScopeItem[] {
|
||||
const normalized = nextScopes.map((scope) => {
|
||||
if (scope.name.trim() !== "tenant") {
|
||||
return scope;
|
||||
}
|
||||
return {
|
||||
...scope,
|
||||
description: scope.description || tenantScopeDescription,
|
||||
mandatory: restricted ? true : false,
|
||||
locked: restricted,
|
||||
};
|
||||
});
|
||||
|
||||
if (restricted && !normalized.some((scope) => scope.name.trim() === "tenant")) {
|
||||
normalized.push(buildTenantScope(`tenant-${Date.now()}`));
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
const handleTenantAccessToggle = (enabled: boolean) => {
|
||||
setTenantAccessRestricted(enabled);
|
||||
setScopes((current) => normalizeScopesForTenantAccess(current, enabled));
|
||||
};
|
||||
|
||||
const toggleAllowedTenant = (tenantId: string) => {
|
||||
setAllowedTenantIds((current) =>
|
||||
current.includes(tenantId)
|
||||
? current.filter((id) => id !== tenantId)
|
||||
: [...current, tenantId],
|
||||
);
|
||||
};
|
||||
|
||||
const addScope = () => {
|
||||
const newId = String(Date.now());
|
||||
setScopes([
|
||||
@@ -292,15 +381,23 @@ function ClientGeneralPage() {
|
||||
field: K,
|
||||
value: ScopeItem[K],
|
||||
) => {
|
||||
setScopes(
|
||||
scopes.map((scope) =>
|
||||
scope.id === id ? { ...scope, [field]: value } : scope,
|
||||
),
|
||||
setScopes((current) =>
|
||||
current.map((scope) => {
|
||||
if (scope.id !== id) {
|
||||
return scope;
|
||||
}
|
||||
if (scope.locked) {
|
||||
return scope;
|
||||
}
|
||||
return { ...scope, [field]: value };
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const removeScope = (id: string) => {
|
||||
setScopes(scopes.filter((s) => s.id !== id));
|
||||
setScopes((current) =>
|
||||
current.filter((scope) => scope.id !== id || scope.locked === true),
|
||||
);
|
||||
};
|
||||
|
||||
const handleStatusChange = (nextStatus: ClientStatus) => {
|
||||
@@ -391,7 +488,25 @@ function ClientGeneralPage() {
|
||||
}
|
||||
}
|
||||
|
||||
if (tenantAccessRestricted && allowedTenantIds.length === 0) {
|
||||
validationErrors.push(
|
||||
t(
|
||||
"ui.dev.clients.general.tenant_access.validation_required",
|
||||
"테넌트 접근 제한을 사용할 경우 허용 테넌트를 하나 이상 선택해야 합니다.",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const hasValidationErrors = validationErrors.length > 0;
|
||||
const normalizedTenantSearch = tenantSearch.trim().toLowerCase();
|
||||
const tenantOptions: TenantSummary[] = tenantData?.items ?? [];
|
||||
const filteredTenants = tenantOptions.filter((tenant) => {
|
||||
if (!normalizedTenantSearch) {
|
||||
return true;
|
||||
}
|
||||
const searchable = `${tenant.name} ${tenant.slug} ${tenant.description} ${tenant.type}`.toLowerCase();
|
||||
return searchable.includes(normalizedTenantSearch);
|
||||
});
|
||||
|
||||
const refreshHeadlessJwksCacheMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
@@ -467,7 +582,16 @@ function ClientGeneralPage() {
|
||||
);
|
||||
}
|
||||
|
||||
const scopeNames = scopes.map((scope) => scope.name).filter(Boolean);
|
||||
const normalizedScopes = normalizeScopesForTenantAccess(
|
||||
scopes,
|
||||
tenantAccessRestricted,
|
||||
);
|
||||
const normalizedAllowedTenantIds = Array.from(
|
||||
new Set(allowedTenantIds.map((id) => id.trim()).filter(Boolean)),
|
||||
);
|
||||
const scopeNames = normalizedScopes
|
||||
.map((scope) => scope.name.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
const effectiveTokenEndpointAuthMethod =
|
||||
clientType === "pkce" && headlessLoginEnabled
|
||||
@@ -487,7 +611,7 @@ function ClientGeneralPage() {
|
||||
metadata: {
|
||||
description,
|
||||
logo_url: trimmedLogoUrl,
|
||||
structured_scopes: scopes,
|
||||
structured_scopes: normalizedScopes,
|
||||
token_endpoint_auth_method: effectiveTokenEndpointAuthMethod,
|
||||
headless_login_enabled: headlessLoginEnabled,
|
||||
headless_token_endpoint_auth_method:
|
||||
@@ -498,6 +622,10 @@ function ClientGeneralPage() {
|
||||
clientType === "pkce" && headlessLoginEnabled
|
||||
? trimmedJwksUri
|
||||
: undefined,
|
||||
tenant_access_restricted: tenantAccessRestricted,
|
||||
allowed_tenants: tenantAccessRestricted
|
||||
? normalizedAllowedTenantIds
|
||||
: [],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -972,7 +1100,10 @@ function ClientGeneralPage() {
|
||||
{scopes.map((s) => (
|
||||
<tr
|
||||
key={s.id}
|
||||
className="hover:bg-muted/30 transition-colors"
|
||||
className={cn(
|
||||
"transition-colors",
|
||||
s.locked ? "bg-primary/5" : "hover:bg-muted/30",
|
||||
)}
|
||||
>
|
||||
<td className="px-4 py-3">
|
||||
<Input
|
||||
@@ -985,6 +1116,7 @@ function ClientGeneralPage() {
|
||||
"ui.dev.clients.general.scopes.name_placeholder",
|
||||
"e.g. profile",
|
||||
)}
|
||||
disabled={s.locked}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
@@ -998,6 +1130,7 @@ function ClientGeneralPage() {
|
||||
"ui.dev.clients.general.scopes.description_placeholder",
|
||||
"권한에 대한 설명",
|
||||
)}
|
||||
disabled={s.locked}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-center">
|
||||
@@ -1007,6 +1140,7 @@ function ClientGeneralPage() {
|
||||
onCheckedChange={(checked) =>
|
||||
updateScope(s.id, "mandatory", checked)
|
||||
}
|
||||
disabled={s.locked}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
@@ -1016,6 +1150,7 @@ function ClientGeneralPage() {
|
||||
size="icon"
|
||||
onClick={() => removeScope(s.id)}
|
||||
className="h-8 w-8 text-muted-foreground hover:text-destructive"
|
||||
disabled={s.locked}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -1041,6 +1176,187 @@ 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>
|
||||
<CardDescription>
|
||||
{t(
|
||||
"ui.dev.clients.general.tenant_access.subtitle",
|
||||
"허용된 테넌트만 이 RP에 접근할 수 있도록 제한합니다.",
|
||||
)}
|
||||
</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">
|
||||
{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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-5">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t(
|
||||
"ui.dev.clients.general.tenant_access.hint",
|
||||
"제한을 켜면 tenant 스코프가 자동으로 포함되며, 허용 테넌트를 하나 이상 선택해야 합니다.",
|
||||
)}
|
||||
</p>
|
||||
|
||||
<div className="grid gap-4 lg:grid-cols-[1.2fr_0.8fr]">
|
||||
<div className="space-y-3">
|
||||
<Label htmlFor="tenant-search" className="text-sm font-semibold">
|
||||
{t(
|
||||
"ui.dev.clients.general.tenant_access.search_placeholder",
|
||||
"테넌트 이름 또는 슬러그로 검색",
|
||||
)}
|
||||
</Label>
|
||||
<Input
|
||||
id="tenant-search"
|
||||
value={tenantSearch}
|
||||
onChange={(e) => setTenantSearch(e.target.value)}
|
||||
placeholder={t(
|
||||
"ui.dev.clients.general.tenant_access.search_placeholder",
|
||||
"테넌트 이름 또는 슬러그로 검색",
|
||||
)}
|
||||
disabled={!tenantAccessRestricted}
|
||||
/>
|
||||
<ScrollArea className="h-72 rounded-xl border border-border bg-card">
|
||||
<div className="divide-y divide-border">
|
||||
{tenantAccessRestricted ? (
|
||||
filteredTenants.length > 0 ? (
|
||||
filteredTenants.map((tenant) => {
|
||||
const checked = allowedTenantIds.includes(tenant.id);
|
||||
return (
|
||||
<label
|
||||
key={tenant.id}
|
||||
className="flex cursor-pointer items-center justify-between gap-4 px-4 py-3 transition hover:bg-muted/30"
|
||||
>
|
||||
<div className="min-w-0 space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="truncate font-medium">
|
||||
{tenant.name}
|
||||
</span>
|
||||
<Badge variant="outline" className="text-[11px]">
|
||||
{tenant.slug}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{tenant.description || tenant.type}
|
||||
</p>
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
onChange={() => toggleAllowedTenant(tenant.id)}
|
||||
className="h-4 w-4 rounded border-border text-primary focus:ring-primary"
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="px-4 py-8 text-center text-sm text-muted-foreground">
|
||||
{t(
|
||||
"ui.dev.clients.general.tenant_access.empty",
|
||||
"검색 결과가 없습니다.",
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="px-4 py-8 text-center text-sm text-muted-foreground">
|
||||
{t(
|
||||
"ui.dev.clients.general.tenant_access.disabled",
|
||||
"제한 없음",
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-semibold">
|
||||
{t(
|
||||
"ui.dev.clients.general.tenant_access.selected_title",
|
||||
"허용 테넌트",
|
||||
)}
|
||||
</Label>
|
||||
<div className="min-h-72 rounded-xl border border-border bg-muted/20 p-3">
|
||||
{tenantAccessRestricted && allowedTenantIds.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{allowedTenantIds.map((tenantId) => {
|
||||
const tenant = tenantData?.items.find(
|
||||
(item) => item.id === tenantId,
|
||||
);
|
||||
return (
|
||||
<Badge
|
||||
key={tenantId}
|
||||
variant="secondary"
|
||||
className="gap-2 px-3 py-1.5"
|
||||
>
|
||||
<span className="max-w-44 truncate">
|
||||
{tenant?.name || tenantId}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t(
|
||||
"ui.common.delete",
|
||||
"삭제",
|
||||
)}
|
||||
onClick={() => toggleAllowedTenant(tenantId)}
|
||||
className="text-xs font-semibold text-muted-foreground hover:text-destructive"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</Badge>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-full min-h-64 items-center justify-center text-sm text-muted-foreground">
|
||||
{tenantAccessRestricted
|
||||
? t(
|
||||
"ui.dev.clients.general.tenant_access.selected_empty",
|
||||
"아직 선택된 테넌트가 없습니다.",
|
||||
)
|
||||
: t(
|
||||
"ui.dev.clients.general.tenant_access.disabled",
|
||||
"제한 없음",
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 3. Security Settings */}
|
||||
<Card className="glass-panel">
|
||||
<CardHeader className="pb-3">
|
||||
|
||||
@@ -23,6 +23,28 @@ export type ClientListResponse = {
|
||||
offset: number;
|
||||
};
|
||||
|
||||
export type TenantSummary = {
|
||||
id: string;
|
||||
type: string;
|
||||
parentId?: string | null;
|
||||
name: string;
|
||||
slug: string;
|
||||
description: string;
|
||||
status: string;
|
||||
domains?: string[];
|
||||
config?: Record<string, unknown>;
|
||||
memberCount: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type TenantListResponse = {
|
||||
items: TenantSummary[];
|
||||
limit: number;
|
||||
offset: number;
|
||||
total: number;
|
||||
};
|
||||
|
||||
export type DevStats = {
|
||||
total_clients: number;
|
||||
active_sessions: number;
|
||||
@@ -188,6 +210,17 @@ export async function fetchDevStats() {
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function fetchTenants(
|
||||
limit = 1000,
|
||||
offset = 0,
|
||||
parentId?: string,
|
||||
) {
|
||||
const { data } = await apiClient.get<TenantListResponse>("/tenants", {
|
||||
params: { limit, offset, parentId },
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function fetchClient(clientId: string) {
|
||||
const { data } = await apiClient.get<ClientDetailResponse>(
|
||||
`/dev/clients/${clientId}`,
|
||||
@@ -376,14 +409,14 @@ export async function fetchDevAuditLogs(
|
||||
return data;
|
||||
}
|
||||
|
||||
export type TenantSummary = {
|
||||
export type MyTenantSummary = {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
};
|
||||
|
||||
export async function fetchMyTenants() {
|
||||
const { data } = await apiClient.get<TenantSummary[]>("/dev/my-tenants");
|
||||
const { data } = await apiClient.get<MyTenantSummary[]>("/dev/my-tenants");
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
@@ -419,6 +419,7 @@ help = "Enter the redirect URIs. You can modify them in the Federation tab after
|
||||
[msg.dev.clients.general.scopes]
|
||||
empty = "No scopes registered."
|
||||
subtitle = "Define the permission scopes this application can request."
|
||||
tenant = "Tenant access claim"
|
||||
|
||||
[msg.dev.clients.general.security]
|
||||
private_help = "Server side App: For apps that can safely store a client secret, such as Node.js or Java servers."
|
||||
@@ -1436,6 +1437,19 @@ description = "Scope Description"
|
||||
mandatory = "Mandatory"
|
||||
name = "Scope Name"
|
||||
delete = "Delete"
|
||||
tenant = "Tenant"
|
||||
|
||||
[ui.dev.clients.general.tenant_access]
|
||||
title = "Tenant access restriction"
|
||||
subtitle = "Limit this RP so only approved tenants can access it."
|
||||
enabled = "Restricted"
|
||||
disabled = "Unrestricted"
|
||||
search_placeholder = "Search by tenant name or slug"
|
||||
selected_title = "Allowed tenants"
|
||||
selected_empty = "No tenants selected yet."
|
||||
empty = "No tenants match your search."
|
||||
hint = "Turning this on adds the tenant scope automatically and requires at least one allowed tenant."
|
||||
validation_required = "Select at least one allowed tenant when tenant access restriction is enabled."
|
||||
|
||||
[ui.dev.clients.general.security]
|
||||
private = "Server Side App"
|
||||
|
||||
@@ -419,6 +419,7 @@ help = "인증 후 리다이렉트될 URI를 입력하세요. 생성 후 연동
|
||||
[msg.dev.clients.general.scopes]
|
||||
empty = "등록된 스코프가 없습니다."
|
||||
subtitle = "이 앱이 요청할 수 있는 권한 범위를 정의합니다."
|
||||
tenant = "소속 테넌트 정보 접근"
|
||||
|
||||
[msg.dev.clients.general.security]
|
||||
pkce_help = "PKCE 앱 (SPA/모바일): 브라우저나 앱처럼 비밀키를 보관하기 어려운 경우 사용하며, PKCE가 강제됩니다."
|
||||
@@ -1435,6 +1436,19 @@ description = "설명"
|
||||
mandatory = "필수"
|
||||
name = "스코프 이름"
|
||||
delete = "삭제"
|
||||
tenant = "테넌트"
|
||||
|
||||
[ui.dev.clients.general.tenant_access]
|
||||
title = "테넌트 접근 제한"
|
||||
subtitle = "허용된 테넌트만 이 RP에 접근할 수 있도록 제한합니다."
|
||||
enabled = "제한 있음"
|
||||
disabled = "제한 없음"
|
||||
search_placeholder = "테넌트 이름 또는 슬러그로 검색"
|
||||
selected_title = "허용 테넌트"
|
||||
selected_empty = "아직 선택된 테넌트가 없습니다."
|
||||
empty = "검색 결과가 없습니다."
|
||||
hint = "제한을 켜면 tenant 스코프가 자동으로 포함되며, 허용 테넌트를 하나 이상 선택해야 합니다."
|
||||
validation_required = "테넌트 접근 제한을 사용할 경우 허용 테넌트를 하나 이상 선택해야 합니다."
|
||||
|
||||
[ui.dev.clients.general.security]
|
||||
private = "Server side App"
|
||||
|
||||
@@ -465,6 +465,7 @@ help = ""
|
||||
[msg.dev.clients.general.scopes]
|
||||
empty = ""
|
||||
subtitle = ""
|
||||
tenant = ""
|
||||
|
||||
[msg.dev.clients.general.security]
|
||||
private_help = ""
|
||||
@@ -1518,6 +1519,19 @@ description = ""
|
||||
mandatory = ""
|
||||
name = ""
|
||||
delete = ""
|
||||
tenant = ""
|
||||
|
||||
[ui.dev.clients.general.tenant_access]
|
||||
title = ""
|
||||
subtitle = ""
|
||||
enabled = ""
|
||||
disabled = ""
|
||||
search_placeholder = ""
|
||||
selected_title = ""
|
||||
selected_empty = ""
|
||||
empty = ""
|
||||
hint = ""
|
||||
validation_required = ""
|
||||
|
||||
[ui.dev.clients.general.security]
|
||||
private = ""
|
||||
|
||||
Reference in New Issue
Block a user