1
0
forked from baron/baron-sso

code-check 오류 수정

This commit is contained in:
2026-05-04 09:02:36 +09:00
parent 67b3420d00
commit 068d0adbd4
15 changed files with 220 additions and 27 deletions

View File

@@ -56,6 +56,16 @@ import {
} from "./orgChartPicker";
import type { UserSchemaField } from "./userSchemaFields";
type UserSchemaField = {
key: string;
label?: string;
type?: "text" | "number" | "boolean" | "date";
required?: boolean;
adminOnly?: boolean;
validation?: string;
isLoginId?: boolean;
};
type UserFormValues = UserCreateRequest & { metadata: Record<string, unknown> };
type UserType = "hanmac" | "external" | "personal";

View File

@@ -79,6 +79,16 @@ import {
} from "./orgChartPicker";
import type { UserSchemaField } from "./userSchemaFields";
type UserSchemaField = {
key: string;
label?: string;
type?: "text" | "number" | "boolean" | "date";
required?: boolean;
adminOnly?: boolean;
validation?: string;
isLoginId?: boolean;
};
type UserFormValues = Omit<UserUpdateRequest, "metadata"> & {
metadata: Record<string, Record<string, string | number | boolean>>;
};

View File

@@ -119,6 +119,7 @@ test.describe("Tenants Management", () => {
test("should export and import tenant CSV without organization/user combined import", async ({
page,
browserName,
}, testInfo) => {
let exportRequested = false;
let exportUrl = "";
@@ -213,9 +214,12 @@ test.describe("Tenants Management", () => {
/갱신 1|Updated 1/i,
);
expect(importRequested).toBe(true);
if (testInfo.project.name !== "webkit") {
expect(importBody).toContain('filename="tenants.csv"');
if (browserName !== "webkit") {
if (testInfo.project.name !== "webkit") {
expect(importBody).toContain("tenant-alpha-id");
}
}
});
test("should resolve tenant CSV conflicts by choosing create and remapping parent ids", async ({

View File

@@ -192,7 +192,7 @@ func TestDevHandler_Isolation(t *testing.T) {
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
})
t.Run("UpdateClient should enforce tenant isolation", func(t *testing.T) {
t.Run("UpdateClient should require direct edit permission within tenant isolation", func(t *testing.T) {
app := fiber.New()
tenantA := "tenant-a"
app.Use(func(c *fiber.Ctx) error {
@@ -209,11 +209,11 @@ func TestDevHandler_Isolation(t *testing.T) {
"client_name": "Updated Name",
})
// Case 1: Same tenant
// Case 1: Same tenant but no direct edit_config permission
req := httptest.NewRequest(http.MethodPut, "/api/v1/dev/clients/client-tenant-a", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, _ := app.Test(req, -1)
assert.Equal(t, http.StatusOK, resp.StatusCode)
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
// Case 2: Different tenant
req = httptest.NewRequest(http.MethodPut, "/api/v1/dev/clients/client-tenant-b", bytes.NewReader(body))

View File

@@ -348,7 +348,9 @@ function AuditLogsPage() {
) : null}
</div>
</TableCell>
<TableCell className="text-xs">{actionLabel}</TableCell>
<TableCell className="text-xs">
{actionLabel}
</TableCell>
<TableCell className="font-mono text-xs">
<div className="flex items-center gap-2">
<span className="break-all">{targetValue}</span>
@@ -401,16 +403,21 @@ function AuditLogsPage() {
<div className="grid gap-3 md:grid-cols-2">
<div className="space-y-1">
<div>
Request ID: {formatValue(details.request_id)}
Request ID:{" "}
{formatValue(details.request_id)}
</div>
<div>
Method: {formatValue(details.method)}
</div>
<div>Method: {formatValue(details.method)}</div>
<div>Path: {formatValue(details.path)}</div>
<div>
Tenant: {formatValue(details.tenant_id)}
</div>
</div>
<div className="space-y-1 break-all">
<div>Before: {formatValue(details.before)}</div>
<div>
Before: {formatValue(details.before)}
</div>
<div>After: {formatValue(details.after)}</div>
<div>Error: {formatValue(details.error)}</div>
</div>

View File

@@ -218,10 +218,12 @@ function ClientDetailsPage() {
const clientSecret = hasClientSecret
? client?.clientSecret || secretPlaceholder
: t("ui.common.na", "N/A");
const displaySecret =
!hasClientSecret
? t("msg.dev.clients.details.secret_not_applicable", "PKCE 앱에는 Client Secret이 없습니다.")
: clientSecret === secretPlaceholder
const displaySecret = !hasClientSecret
? t(
"msg.dev.clients.details.secret_not_applicable",
"PKCE 앱에는 Client Secret이 없습니다.",
)
: clientSecret === secretPlaceholder
? t("msg.dev.clients.details.secret_unavailable", "SECRET_NOT_AVAILABLE")
: clientSecret;

View File

@@ -463,9 +463,7 @@ function ClientGeneralPage() {
(item.relation === "admins" || item.relation === "config_editor"),
) === true;
const isGeneralSettingsReadOnly =
!isCreate &&
relationData != null &&
!canEditExistingClientGeneralSettings;
!isCreate && relationData != null && !canEditExistingClientGeneralSettings;
const trimmedLogoUrl = logoUrl.trim();
const trimmedAutoLoginUrl = autoLoginUrl.trim();
const hasLogoUrl = trimmedLogoUrl.length > 0;

View File

@@ -312,10 +312,13 @@ function ClientRelationsPage() {
}
};
const handleInfoToggle = (event: React.MouseEvent, relation: RelationOption) => {
const handleInfoToggle = (
event: React.MouseEvent,
relation: RelationOption,
) => {
event.preventDefault();
event.stopPropagation();
setInfoRelation(prev => (prev === relation ? null : relation));
setInfoRelation((prev) => (prev === relation ? null : relation));
};
if (!clientId) {
@@ -401,7 +404,10 @@ function ClientRelationsPage() {
<CardContent className="space-y-6">
{isLoading ? (
<div className="py-8 text-center text-sm text-muted-foreground">
{t("msg.dev.clients.relationships.loading", "Loading relationships...")}
{t(
"msg.dev.clients.relationships.loading",
"Loading relationships...",
)}
</div>
) : isRelationshipViewForbidden ? (
<div className="rounded-md border border-border bg-muted/30 p-4 text-sm text-muted-foreground">
@@ -510,7 +516,7 @@ function ClientRelationsPage() {
selectedUserExistingRelations.has(relation);
const isSelected = selectedRelations.includes(relation);
const isInfoVisible = infoRelation === relation;
return (
<div key={relation} className="relative">
<label
@@ -568,7 +574,7 @@ function ClientRelationsPage() {
</div>
</div>
</label>
{isInfoVisible && (
<div className="mt-2 animate-in fade-in slide-in-from-top-1 rounded-lg border border-primary/20 bg-primary/5 p-3 text-xs leading-relaxed text-foreground shadow-sm">
<div className="flex items-center gap-1.5 font-bold text-primary mb-1">
@@ -673,7 +679,9 @@ function ClientRelationsPage() {
<div className="space-y-1">
<div className="flex flex-col gap-1.5">
<div className="flex items-center gap-2 font-medium">
<span>{relationLabel(item.relation as RelationOption)}</span>
<span>
{relationLabel(item.relation as RelationOption)}
</span>
<button
type="button"
className={`rounded-full p-0.5 transition-colors ${
@@ -681,14 +689,21 @@ function ClientRelationsPage() {
? "text-primary"
: "text-muted-foreground/60 hover:text-primary"
}`}
onClick={(e) => handleInfoToggle(e, item.relation as RelationOption)}
onClick={(e) =>
handleInfoToggle(
e,
item.relation as RelationOption,
)
}
>
<Info className="h-3.5 w-3.5" />
</button>
</div>
{infoRelation === item.relation && (
<div className="animate-in fade-in slide-in-from-top-1 rounded border border-primary/20 bg-primary/5 p-2 text-[11px] leading-relaxed text-foreground max-w-[250px]">
{relationPermitsInfo(item.relation as RelationOption)}
{relationPermitsInfo(
item.relation as RelationOption,
)}
</div>
)}
</div>

View File

@@ -191,11 +191,17 @@ delete_confirm = ""
delete_success = ""
empty = ""
fetch_error = ""
import_empty = ""
import_error = ""
import_result = ""
missing_id = ""
not_found = ""
remove_sub_confirm = ""
subtitle = ""
[msg.admin.tenants.import_preview]
description = ""
[msg.admin.tenants.admins]
add_success = ""
empty = ""
@@ -255,9 +261,13 @@ parsed_count = ""
update_success = ""
[msg.admin.users.create]
appointment_required = ""
error = ""
external_tenant_required = ""
password_required = ""
personal_tenant_failed = ""
success = ""
tenant_resolve_failed = ""
[msg.admin.users.create.account]
subtitle = ""
@@ -269,6 +279,7 @@ field_required = ""
name_required = ""
password_auto_help = ""
password_manual_help = ""
picker_description = ""
role_help = ""
[msg.admin.users.create.password_generated]
@@ -291,7 +302,9 @@ password_hint = ""
[msg.admin.users.list]
delete_confirm = ""
empty = ""
export_error = ""
fetch_error = ""
status_error = ""
subtitle = ""
[msg.admin.users.list.columns]
@@ -991,6 +1004,9 @@ user = ""
[ui.admin.tenants]
add = ""
csv_template = ""
export = ""
import = ""
title = ""
[ui.admin.tenants.admins]
@@ -1120,6 +1136,15 @@ search_placeholder = ""
title = ""
tree_search_placeholder = ""
[ui.admin.tenants.import_preview]
candidates = ""
confirm = ""
create_new = ""
fixed_id = ""
match = ""
no_candidates = ""
title = ""
[ui.admin.tenants.sub.table]
action = ""
name = ""
@@ -1128,6 +1153,7 @@ status = ""
[ui.admin.tenants.table]
actions = ""
id = ""
members = ""
name = ""
slug = ""
@@ -1227,6 +1253,7 @@ empty = ""
fetch_error = ""
search_placeholder = ""
subtitle = ""
toggle_status = ""
title = ""
[ui.admin.users.list.breadcrumb]

View File

@@ -221,6 +221,39 @@ function parseClientId(pathname: string): string {
}
export async function installDevApiMock(page: Page, state: DevApiMockState) {
const readMockRole = async () =>
(
(await page.evaluate(() => window.localStorage.getItem("dev_role"))) ??
"rp_admin"
).trim();
const buildSelfConfigEditorRelation = (): ClientRelation => ({
relation: "config_editor",
subject: "User:playwright-user",
subjectType: "User",
subjectId: "playwright-user",
userName: "Playwright User",
userEmail: "playwright@example.com",
userLoginId: "playwright@example.com",
});
const shouldGrantDefaultEditRelation = (role: string) =>
role === "rp_admin" || role === "tenant_admin" || role === "super_admin";
const resolveClientRelations = async (clientId: string) => {
const explicitRelations = state.relations?.[clientId];
if (explicitRelations) {
return explicitRelations;
}
const role = await readMockRole();
if (!shouldGrantDefaultEditRelation(role)) {
return [];
}
return [buildSelfConfigEditorRelation()];
};
const appendAuditLog = (
eventType: string,
action: string,
@@ -431,6 +464,10 @@ export async function installDevApiMock(page: Page, state: DevApiMockState) {
});
state.clients.push(created);
if (!state.relations) {
state.relations = {};
}
state.relations[created.id] = [buildSelfConfigEditorRelation()];
appendAuditLog("CLIENT_CREATE", "CREATE_CLIENT", created.id);
return json(route, {
client: created,
@@ -451,7 +488,7 @@ export async function installDevApiMock(page: Page, state: DevApiMockState) {
) {
const clientId = pathname.split("/")[5] ?? "";
return json(route, {
items: state.relations?.[clientId] ?? [],
items: await resolveClientRelations(clientId),
});
}

View File

@@ -156,4 +156,4 @@
"authorizer": { "handler": "allow" },
"mutators": [{ "handler": "noop" }]
}
]
]

View File

@@ -1204,6 +1204,15 @@ title = "Tenant Members ({{count}})"
total = "Total"
total_label = "Total"
[ui.admin.tenants.import_preview]
candidates = "Candidates"
confirm = "Confirm Import"
create_new = "Create New"
fixed_id = "Fixed ID"
match = "Matched Tenant"
no_candidates = "No matching tenants found."
title = "Import Preview"
[ui.admin.tenants.members.table]
email = "EMAIL"
name = "NAME"
@@ -1339,6 +1348,7 @@ name = "Name"
name_placeholder = "Name Placeholder"
password = "Password"
password_placeholder = "********"
picker_description = "Search and select a tenant."
phone = "Phone number"
phone_placeholder = "010-1234-5678"
position = "Position"

View File

@@ -672,11 +672,17 @@ delete_confirm = "테넌트 \\\\\\\"{{name}}\\\\\\\"를 삭제할까요?"
delete_success = "테넌트가 삭제되었습니다."
empty = "아직 등록된 테넌트가 없습니다."
fetch_error = "테넌트 목록 조회에 실패했습니다."
import_empty = "임포트 파일에 테넌트 행이 없습니다."
import_error = "테넌트 임포트에 실패했습니다: {{error}}"
import_result = "{{count}}개의 테넌트 행을 처리했습니다."
missing_id = "테넌트 ID가 없습니다."
not_found = "테넌트를 찾을 수 없습니다."
remove_sub_confirm = "테넌트 \\\\\\\"{{name}}\\\\\\\"을(를) 하위 조직에서 제외할까요?"
subtitle = "현재 등록된 테넌트를 확인하고 상태를 관리합니다."
[msg.admin.tenants.import_preview]
description = "임포트 전에 각 행의 매칭 결과를 검토하고 처리 방식을 선택하세요."
[msg.admin.tenants.admins]
add_success = "관리자가 추가되었습니다."
empty = "등록된 관리자가 없습니다."
@@ -1645,6 +1651,8 @@ delete_bulk_confirm = "선택한 {{count}}개 테넌트를 삭제할까요?"
[msg.admin.users]
self_delete_blocked = "자신의 계정은 삭제할 수 없습니다."
export_error = "사용자 내보내기에 실패했습니다: {{error}}"
status_error = "사용자 상태 변경에 실패했습니다: {{error}}"
[ui.admin.apikeys.registry]
title = "API Key Registry"
@@ -1658,6 +1666,15 @@ title = "테넌트 구성원 ({{count}})"
total = "전체"
total_label = "전체"
[ui.admin.tenants.import_preview]
candidates = "후보"
confirm = "임포트 확정"
create_new = "새로 생성"
fixed_id = "고정 ID"
match = "매칭된 테넌트"
no_candidates = "매칭 가능한 테넌트가 없습니다."
title = "임포트 미리보기"
[ui.admin.tenants.members.table]
email = "EMAIL"
name = "NAME"
@@ -1793,6 +1810,7 @@ name = "이름"
name_placeholder = "홍길동"
password = "비밀번호"
password_placeholder = "********"
picker_description = "배정할 테넌트를 검색해서 선택하세요."
phone = "전화번호"
phone_placeholder = "010-1234-5678"
position = "직급"

View File

@@ -1517,9 +1517,17 @@ import_partial_success = ""
[msg.admin.tenants]
delete_bulk_confirm = ""
import_empty = ""
import_error = ""
import_result = ""
[msg.admin.tenants.import_preview]
description = ""
[msg.admin.users]
self_delete_blocked = ""
export_error = ""
status_error = ""
[ui.admin.apikeys.registry]
title = ""
@@ -1604,6 +1612,15 @@ search_placeholder = ""
title = ""
tree_search_placeholder = ""
[ui.admin.tenants.import_preview]
candidates = ""
confirm = ""
create_new = ""
fixed_id = ""
match = ""
no_candidates = ""
title = ""
[ui.admin.tenants.sub.table]
action = ""
name = ""
@@ -1668,6 +1685,7 @@ name = ""
name_placeholder = ""
password = ""
password_placeholder = ""
picker_description = ""
phone = ""
phone_placeholder = ""
position = ""

View File

@@ -6,8 +6,40 @@ job_name="${1:-adminfront-tests}"
mkdir -p reports
rm -rf adminfront/node_modules
playwright_install_cmd=(npx playwright install --with-deps)
playwright_install_desc="npx playwright install --with-deps"
is_port_available() {
local port="$1"
node -e '
const net = require("net");
const port = Number(process.argv[1]);
const server = net.createServer();
server.once("error", () => process.exit(1));
server.once("listening", () => server.close(() => process.exit(0)));
server.listen(port, "127.0.0.1");
' "$port"
}
find_available_port() {
node -e '
const net = require("net");
const server = net.createServer();
server.listen(0, "127.0.0.1", () => {
const address = server.address();
process.stdout.write(String(address.port));
server.close();
});
'
}
playwright_install_cmd=(npx playwright install)
playwright_install_desc="npx playwright install"
if [ "$(id -u)" -eq 0 ]; then
playwright_install_cmd=(npx playwright install --with-deps)
playwright_install_desc="npx playwright install --with-deps"
elif command -v sudo >/dev/null 2>&1 && sudo -n true >/dev/null 2>&1; then
playwright_install_cmd=(npx playwright install --with-deps)
playwright_install_desc="npx playwright install --with-deps"
fi
set +e
(
@@ -67,6 +99,11 @@ fi
set +e
port="${PORT:-5180}"
if ! is_port_available "$port"; then
fallback_port="$(find_available_port)"
echo "==> requested PORT=$port is already in use; switching to PORT=$fallback_port"
port="$fallback_port"
fi
echo "==> adminfront using PORT=$port"
(
cd adminfront