1
0
forked from baron/baron-sso

코드체크 업데이트

This commit is contained in:
2026-05-12 13:41:43 +09:00
parent 5e649c279f
commit d4c48da426
32 changed files with 486 additions and 85 deletions

View File

@@ -174,7 +174,7 @@ jobs:
# 코드 변경 반영을 위해 build 수행 (userfront nginx.conf 등)
docker compose -f staging_pull_compose.yaml build --pull
docker compose -f staging_pull_compose.yaml up -d --remove-orphans
docker compose -f staging_pull_compose.yaml up -d --remove-orphans --renew-anon-volumes
docker compose -f staging_pull_compose.yaml up -d --force-recreate kratos hydra keto oathkeeper
docker compose -f staging_pull_compose.yaml up -d --force-recreate ory_stack_check
docker compose -f staging_pull_compose.yaml up -d init-rp

View File

@@ -1,5 +1,12 @@
import { createRequire } from "node:module";
import { defineConfig, devices } from "@playwright/test";
const require = createRequire(import.meta.url);
const { shouldIncludeWebKit } =
require("../scripts/playwrightHostDeps.cjs") as {
shouldIncludeWebKit: () => boolean;
};
const configuredWorkers = process.env.PLAYWRIGHT_WORKERS
? Number.parseInt(process.env.PLAYWRIGHT_WORKERS, 10)
: undefined;
@@ -57,10 +64,14 @@ export default defineConfig({
use: { ...devices["Desktop Firefox"] },
},
{
name: "webkit",
use: { ...devices["Desktop Safari"] },
},
...(shouldIncludeWebKit()
? [
{
name: "webkit",
use: { ...devices["Desktop Safari"] },
},
]
: []),
],
/* Run your local dev server before starting the tests */

View File

@@ -35,6 +35,29 @@ if [ "${1:-}" = "--print-mode" ]; then
exit 0
fi
ensure_frontend_dependencies() {
if [ ! -f package.json ] || [ ! -f package-lock.json ]; then
return 0
fi
if command -v sha256sum >/dev/null 2>&1; then
deps_hash="$(sha256sum package.json package-lock.json | sha256sum | awk '{print $1}')"
else
deps_hash="$(cksum package.json package-lock.json | cksum | awk '{print $1}')"
fi
deps_stamp="node_modules/.baron-deps-hash"
installed_hash="$(cat "$deps_stamp" 2>/dev/null || true)"
if [ "$installed_hash" != "$deps_hash" ]; then
echo "Installing frontend dependencies from package-lock.json..."
npm ci
mkdir -p node_modules
printf '%s\n' "$deps_hash" > "$deps_stamp"
fi
}
ensure_frontend_dependencies
if [ "$mode" = "production" ]; then
echo "Running in production mode with Vite preview..."
exec sh -c "npm run build && npm run preview -- --host 0.0.0.0"

View File

@@ -101,18 +101,10 @@ export function ParentTenantSelector({
return (
<div className="space-y-2">
<div className="flex min-h-8 flex-wrap items-center justify-between gap-2">
<Label className="text-sm font-semibold">
{label}
</Label>
<Label className="text-sm font-semibold">{label}</Label>
{labelAction}
</div>
<input
id={id}
name={id}
type="hidden"
value={value}
readOnly
/>
<input id={id} name={id} type="hidden" value={value} readOnly />
<div className="flex min-h-10 flex-wrap items-center gap-2 rounded-md border border-input bg-background px-3 py-2">
<Button
type="button"

View File

@@ -63,7 +63,10 @@ function TenantCreatePage() {
parentStepConfirmed || Boolean(selectedParentTenant);
const parentContextLabel = selectedParentTenant
? canConfigureHanmacOrg
? t("ui.admin.tenants.create.parent_context.hanmac", "한맥가족 하위 테넌트")
? t(
"ui.admin.tenants.create.parent_context.hanmac",
"한맥가족 하위 테넌트",
)
: t("ui.admin.tenants.create.parent_context.general", "일반 하위 테넌트")
: parentStepConfirmed
? t("ui.admin.tenants.create.parent_context.root", "최상위 테넌트")
@@ -232,10 +235,7 @@ function TenantCreatePage() {
))}
</select>
</div>
<div
data-testid="tenant-visibility-slot"
className="space-y-2"
>
<div data-testid="tenant-visibility-slot" className="space-y-2">
<Label
htmlFor="tenant-visibility"
className="text-sm font-semibold"

View File

@@ -980,7 +980,7 @@ function TenantListPage() {
</Button>
</TableCell>
</TableRow>
))}{" "}
))}
</TableBody>
</Table>
</div>

View File

@@ -338,10 +338,7 @@ export function TenantProfilePage() {
))}
</select>
</div>
<div
data-testid="tenant-visibility-slot"
className="space-y-2"
>
<div data-testid="tenant-visibility-slot" className="space-y-2">
<Label className="text-sm font-semibold">
{t("ui.admin.tenants.profile.visibility", "공개 범위")}
</Label>

View File

@@ -202,6 +202,11 @@ subtitle = "Subtitle"
[msg.admin.tenants.import_preview]
description = "Rows without tenant_id are compared with existing tenant candidates, then imported as new tenants or updates."
[msg.admin.tenants.parent]
local_picker_description = "Select the tenant to use as the parent from the tenant list."
local_picker_empty = "No selectable tenants are available."
picker_description = "Select a tenant in org-chart to apply it as the parent tenant."
[msg.admin.tenants.admins]
add_success = "Add Success"
empty = "Empty"
@@ -217,6 +222,7 @@ remove_success = "Owner permission revoked."
subtitle = "List of owners with top-level permissions for this tenant."
[msg.admin.tenants.create]
pick_parent_first = "Select the parent tenant first."
subtitle = "Subtitle"
[msg.admin.tenants.create.form]
@@ -908,9 +914,14 @@ title = "Domain conflict"
candidates = "Candidates"
confirm = "Run import"
create_new_reset = "Create new (reset ID/slug)"
csv_parents = "CSV Parents"
external_id = "External ID"
match = "Match"
no_candidates = "No candidates"
parent = "Parent"
parent_companies = "Parent Companies"
parent_company_groups = "Parent Company Groups"
parent_organizations = "Parent Organizations"
parent_unresolved = "Parent needs review"
slug_exists = "slug conflict"
title = "Confirm CSV import"
@@ -957,11 +968,20 @@ domains_label = "Allowed Domains (Comma separated)"
domains_placeholder = "example.com, example.kr"
name = "Tenant name"
parent = "Parent"
pick_hanmac_parent = "Pick from Hanmac Family"
pick_other_parent = "Pick another tenant"
root_tenant = "Create as top-level tenant"
slug = "Slug"
slug_placeholder = "tenant-slug"
status = "Status"
type = "Type"
[ui.admin.tenants.create.parent_context]
general = "General child tenant"
hanmac = "Hanmac Family child tenant"
pick_required = "Parent tenant selection required"
root = "Top-level tenant"
[ui.admin.tenants.create.memo]
title = "Title"
@@ -1023,11 +1043,17 @@ allowed_domains_help = "Users with these email domains will be automatically ass
approve_button = "Approve Tenant"
description = "Description"
name = "Tenant Name"
org_unit_type = "Organization detail type"
slug = "Slug"
status = "Status"
subtitle = "Slug and status changes are applied immediately."
title = "Tenant Profile"
type = "Type"
visibility = "Visibility"
[ui.admin.tenants.parent]
local_search_placeholder = "Search tenant name or slug"
pick_tenant = "Pick tenant"
[ui.admin.tenants.registry]
title = "Tenant registry"

View File

@@ -203,6 +203,11 @@ subtitle = "현재 등록된 테넌트를 확인하고 상태를 관리합니다
[msg.admin.tenants.import_preview]
description = "tenant_id가 없는 행은 기존 테넌트 후보와 비교한 뒤 신규 생성 또는 기존 테넌트 갱신으로 처리합니다."
[msg.admin.tenants.parent]
local_picker_description = "테넌트 목록에서 상위 테넌트로 사용할 항목을 선택합니다."
local_picker_empty = "선택할 수 있는 테넌트가 없습니다."
picker_description = "org-chart에서 테넌트를 선택하면 상위 테넌트에 반영됩니다."
[msg.admin.tenants.admins]
add_success = "관리자가 추가되었습니다."
empty = "등록된 관리자가 없습니다."
@@ -218,6 +223,7 @@ remove_success = "소유자 권한이 회수되었습니다."
subtitle = "이 테넌트의 최상위 권한을 가진 소유자(조직장) 목록입니다."
[msg.admin.tenants.create]
pick_parent_first = "상위 테넌트를 먼저 선택하세요."
subtitle = "글로벌 운영 기준의 신규 테넌트를 등록합니다."
[msg.admin.tenants.create.form]
@@ -910,9 +916,14 @@ title = "도메인 충돌"
candidates = "후보"
confirm = "가져오기 실행"
create_new_reset = "신규 생성 (ID/slug 재설정)"
csv_parents = "CSV 상위 테넌트"
external_id = "외부 ID"
match = "매칭"
no_candidates = "후보 없음"
parent = "상위"
parent_companies = "상위 회사"
parent_company_groups = "상위 그룹사"
parent_organizations = "상위 조직"
parent_unresolved = "부모 확인 필요"
slug_exists = "slug 충돌"
title = "CSV 가져오기 확인"
@@ -959,11 +970,20 @@ domains_label = "Allowed Domains (Comma separated)"
domains_placeholder = "example.com, example.kr"
name = "테넌트 이름"
parent = "상위 테넌트"
pick_hanmac_parent = "한맥가족에서 선택"
pick_other_parent = "다른 테넌트 선택"
root_tenant = "최상위 테넌트로 생성"
slug = "Slug"
slug_placeholder = "tenant-slug"
status = "상태"
type = "유형"
[ui.admin.tenants.create.parent_context]
general = "일반 하위 테넌트"
hanmac = "한맥가족 하위 테넌트"
pick_required = "상위 테넌트 선택 필요"
root = "최상위 테넌트"
[ui.admin.tenants.create.memo]
title = "정책 메모"
@@ -1025,11 +1045,17 @@ allowed_domains_help = "이 도메인을 가진 이메일로 가입한 사용자
approve_button = "테넌트 승인"
description = "설명"
name = "테넌트 이름"
org_unit_type = "조직 세부타입"
slug = "슬러그 (Slug)"
status = "상태"
subtitle = "슬러그 및 상태 변경은 즉시 적용됩니다."
title = "테넌트 프로필"
type = "테넌트 유형"
visibility = "공개 범위"
[ui.admin.tenants.parent]
local_search_placeholder = "테넌트 이름 또는 슬러그 검색"
pick_tenant = "테넌트 선택"
[ui.admin.tenants.registry]
title = "Tenant registry"

View File

@@ -208,6 +208,11 @@ subtitle = ""
[msg.admin.tenants.import_preview]
description = ""
[msg.admin.tenants.parent]
local_picker_description = ""
local_picker_empty = ""
picker_description = ""
[msg.admin.tenants.admins]
add_success = ""
empty = ""
@@ -223,6 +228,7 @@ remove_success = ""
subtitle = ""
[msg.admin.tenants.create]
pick_parent_first = ""
subtitle = ""
[msg.admin.tenants.create.form]
@@ -924,9 +930,14 @@ title = ""
candidates = ""
confirm = ""
create_new_reset = ""
csv_parents = ""
external_id = ""
match = ""
no_candidates = ""
parent = ""
parent_companies = ""
parent_company_groups = ""
parent_organizations = ""
parent_unresolved = ""
slug_exists = ""
title = ""
@@ -973,11 +984,20 @@ domains_label = ""
domains_placeholder = ""
name = ""
parent = ""
pick_hanmac_parent = ""
pick_other_parent = ""
root_tenant = ""
slug = ""
slug_placeholder = ""
status = ""
type = ""
[ui.admin.tenants.create.parent_context]
general = ""
hanmac = ""
pick_required = ""
root = ""
[ui.admin.tenants.create.memo]
title = ""
@@ -1044,11 +1064,17 @@ allowed_domains_help = ""
approve_button = ""
description = ""
name = ""
org_unit_type = ""
slug = ""
status = ""
subtitle = ""
title = ""
type = ""
visibility = ""
[ui.admin.tenants.parent]
local_search_placeholder = ""
pick_tenant = ""
[ui.admin.tenants.registry]
title = ""

View File

@@ -110,9 +110,7 @@ test.describe("Tenants Management", () => {
await expect(page.locator("h2").last()).toContainText(/추가|Create/i, {
timeout: 20000,
});
await page
.getByRole("button", { name: "최상위 테넌트로 생성" })
.click();
await page.getByRole("button", { name: "최상위 테넌트로 생성" }).click();
const nameInput = page.locator('input[name="name"]').first();
await nameInput.fill("New Tenant");
@@ -213,9 +211,7 @@ test.describe("Tenants Management", () => {
{
type: "orgfront:picker:confirm",
payload: {
selections: [
{ type: "tenant", id: "family-1", name: "한맥가족" },
],
selections: [{ type: "tenant", id: "family-1", name: "한맥가족" }],
},
},
window.location.origin,
@@ -260,7 +256,12 @@ test.describe("Tenants Management", () => {
const headers = { "Access-Control-Allow-Origin": "*" };
if (method === "GET") {
return route.fulfill({
json: { items: tenants, total: tenants.length, limit: 1000, offset: 0 },
json: {
items: tenants,
total: tenants.length,
limit: 1000,
offset: 0,
},
headers,
});
}
@@ -286,9 +287,7 @@ test.describe("Tenants Management", () => {
{
type: "orgfront:picker:confirm",
payload: {
selections: [
{ type: "tenant", id: "family-1", name: "한맥가족" },
],
selections: [{ type: "tenant", id: "family-1", name: "한맥가족" }],
},
},
window.location.origin,
@@ -309,8 +308,8 @@ test.describe("Tenants Management", () => {
const visibilityWidth = await page
.getByTestId("tenant-visibility-slot")
.evaluate((element) => element.getBoundingClientRect().width);
const columns = await layout.evaluate((element) =>
window.getComputedStyle(element).gridTemplateColumns,
const columns = await layout.evaluate(
(element) => window.getComputedStyle(element).gridTemplateColumns,
);
expect(columns.split(" ").length).toBe(4);
expect(parentWidth).toBeGreaterThan(orgUnitWidth * 1.7);
@@ -543,9 +542,7 @@ test.describe("Tenants Management", () => {
await expect(page.locator("h2").last()).toContainText(/추가|Create/i, {
timeout: 20000,
});
await page
.getByRole("button", { name: "최상위 테넌트로 생성" })
.click();
await page.getByRole("button", { name: "최상위 테넌트로 생성" }).click();
const submitBtn = page.getByRole("button", { name: /^생성$/ });
await expect(submitBtn).toBeDisabled();
@@ -715,8 +712,8 @@ test.describe("Tenants Management", () => {
await expect(layout).toContainText("조직 세부타입");
await expect(layout).toContainText("공개 범위");
const columns = await layout.evaluate((element) =>
window.getComputedStyle(element).gridTemplateColumns,
const columns = await layout.evaluate(
(element) => window.getComputedStyle(element).gridTemplateColumns,
);
expect(columns.split(" ").length).toBe(4);

View File

@@ -419,6 +419,8 @@ func TestHeadlessPasswordLogin_E2E_ResponseIncludesDetailedCodeAndLogs(t *testin
}
func TestHeadlessPasswordLogin_E2E_DebugLogsIncludeDiagnostics(t *testing.T) {
t.Setenv("BACKEND_PUBLIC_URL", "")
privateKey, jwks := mustE2EHeadlessRSAJWK(t)
const receivedAudience = "https://sso.hmac.kr/api/v1/auth/headless/password/login"
clientAssertion := mustE2EHeadlessClientAssertion(
@@ -458,6 +460,8 @@ func TestHeadlessPasswordLogin_E2E_DebugLogsIncludeDiagnostics(t *testing.T) {
}
func TestHeadlessPasswordLogin_E2E_AcceptsForwardedHTTPSAudience(t *testing.T) {
t.Setenv("BACKEND_PUBLIC_URL", "")
privateKey, jwks := mustE2EHeadlessRSAJWK(t)
const receivedAudience = "https://sso.hmac.kr/api/v1/auth/headless/password/login"
clientAssertion := mustE2EHeadlessClientAssertion(

View File

@@ -894,6 +894,8 @@ func TestHeadlessPasswordLogin_HeadlessLoginClientSuccess(t *testing.T) {
}
func TestHeadlessPasswordLogin_AuditIncludesClientMetadata(t *testing.T) {
t.Setenv("BACKEND_PUBLIC_URL", "")
mockIdp := new(MockIdentityProvider)
mockIdp.On("SignIn", "employee001", "password").Return(&domain.AuthInfo{
SessionToken: &domain.Token{JWT: "valid-jwt", SessionID: "session-123"},

View File

@@ -11,6 +11,8 @@ import (
)
func TestRPManifestJSONIncludesIAMAndExternalKeyContract(t *testing.T) {
t.Setenv("BACKEND_PUBLIC_URL", "")
app := fiber.New()
h := NewRPManifestHandler()
app.Get("/.well-known/baron-rp-manifest.json", h.GetJSON)

View File

@@ -112,6 +112,7 @@ func (m *MockUserRepoForHandler) Delete(ctx context.Context, id string) error {
m.deletedIDs = append(m.deletedIDs, id)
return nil
}
func (m *MockUserRepoForHandler) FindByEmail(ctx context.Context, email string) (*domain.User, error) {
return nil, nil
}

View File

@@ -55,6 +55,7 @@ func (m *MockUserRepository) Update(ctx context.Context, user *domain.User) erro
m.updatedUsers = append(m.updatedUsers, copied)
return nil
}
func (m *MockUserRepository) Delete(ctx context.Context, id string) error {
return m.Called(ctx, id).Error(0)
}

View File

@@ -213,6 +213,8 @@ func TestResolveWorksmobileDomainIDFromTenantIgnoresRootDomainMappings(t *testin
}
func TestResolveWorksmobileDomainIDFromTenantRequiresFamilyDomainEnv(t *testing.T) {
t.Setenv("SAMAN_DOMAIN_ID", "")
rootConfig := domain.JSONMap{
"worksmobile": map[string]any{
"domainMappings": map[string]any{

View File

@@ -1,5 +1,12 @@
import { createRequire } from "node:module";
import { defineConfig, devices } from "@playwright/test";
const require = createRequire(import.meta.url);
const { shouldIncludeWebKit } =
require("../scripts/playwrightHostDeps.cjs") as {
shouldIncludeWebKit: () => boolean;
};
const configuredWorkers = process.env.PLAYWRIGHT_WORKERS
? Number.parseInt(process.env.PLAYWRIGHT_WORKERS, 10)
: undefined;
@@ -52,10 +59,14 @@ export default defineConfig({
use: { ...devices["Desktop Firefox"] },
},
{
name: "webkit",
use: { ...devices["Desktop Safari"] },
},
...(shouldIncludeWebKit()
? [
{
name: "webkit",
use: { ...devices["Desktop Safari"] },
},
]
: []),
],
/* Run your local dev server before starting the tests */

View File

@@ -35,6 +35,29 @@ if [ "${1:-}" = "--print-mode" ]; then
exit 0
fi
ensure_frontend_dependencies() {
if [ ! -f package.json ] || [ ! -f package-lock.json ]; then
return 0
fi
if command -v sha256sum >/dev/null 2>&1; then
deps_hash="$(sha256sum package.json package-lock.json | sha256sum | awk '{print $1}')"
else
deps_hash="$(cksum package.json package-lock.json | cksum | awk '{print $1}')"
fi
deps_stamp="node_modules/.baron-deps-hash"
installed_hash="$(cat "$deps_stamp" 2>/dev/null || true)"
if [ "$installed_hash" != "$deps_hash" ]; then
echo "Installing frontend dependencies from package-lock.json..."
npm ci
mkdir -p node_modules
printf '%s\n' "$deps_hash" > "$deps_stamp"
fi
}
ensure_frontend_dependencies
if [ "$mode" = "production" ]; then
echo "Running in production mode with Vite preview..."
exec sh -c "npm run build && npm run preview -- --host 0.0.0.0"

View File

@@ -208,6 +208,11 @@ subtitle = "Review registered tenants and manage their current status."
[msg.admin.tenants.import_preview]
description = "Rows without tenant_id are compared with existing tenant candidates, then imported as new tenants or updates."
[msg.admin.tenants.parent]
local_picker_description = "Select the tenant to use as the parent from the tenant list."
local_picker_empty = "No selectable tenants are available."
picker_description = "Select a tenant in org-chart to apply it as the parent tenant."
[msg.admin.tenants.admins]
add_success = "Tenant admin added successfully."
empty = "No tenant admins are assigned yet."
@@ -218,6 +223,7 @@ remove_success = "Tenant admin removed successfully."
subtitle = "Manage the administrators assigned to this tenant."
[msg.admin.tenants.create]
pick_parent_first = "Select the parent tenant first."
subtitle = "Enter the minimum required information to create a tenant."
[msg.admin.tenants.create.form]
@@ -1124,9 +1130,14 @@ title = "Domain conflict"
candidates = "Candidates"
confirm = "Run import"
create_new_reset = "Create new (reset ID/slug)"
csv_parents = "CSV Parents"
external_id = "External ID"
match = "Match"
no_candidates = "No candidates"
parent = "Parent"
parent_companies = "Parent Companies"
parent_company_groups = "Parent Company Groups"
parent_organizations = "Parent Organizations"
parent_unresolved = "Parent needs review"
slug_exists = "slug conflict"
title = "Confirm CSV import"
@@ -1163,11 +1174,20 @@ domains_placeholder = "example.com, example.kr"
name = "Tenant name"
name_placeholder = "Enter tenant name"
parent = "Parent"
pick_hanmac_parent = "Pick from Hanmac Family"
pick_other_parent = "Pick another tenant"
root_tenant = "Create as top-level tenant"
slug = "Slug"
slug_placeholder = "tenant-slug"
status = "Status"
type = "Type"
[ui.admin.tenants.create.parent_context]
general = "General child tenant"
hanmac = "Hanmac Family child tenant"
pick_required = "Parent tenant selection required"
root = "Top-level tenant"
[ui.admin.tenants.create.memo]
title = "Policy Memo"
@@ -1249,9 +1269,14 @@ view_profile = "View Profile"
candidates = "Candidates"
confirm = "Confirm Import"
create_new = "Create New"
csv_parents = "CSV Parents"
fixed_id = "Fixed ID"
match = "Matched Tenant"
no_candidates = "No matching tenants found."
parent = "Parent"
parent_companies = "Parent Companies"
parent_company_groups = "Parent Company Groups"
parent_organizations = "Parent Organizations"
title = "Import Preview"
[ui.admin.tenants.members.table]
@@ -1278,16 +1303,22 @@ allowed_domains_help = "Users with these email domains will be automatically ass
approve_button = "Approve Tenant"
description = "Review and edit the tenant's basic profile information."
name = "Tenant Name"
org_unit_type = "Organization detail type"
slug = "Slug"
status = "Status"
subtitle = "Slug and status changes are applied immediately."
title = "Tenant Profile"
type = "Type"
visibility = "Visibility"
[ui.admin.tenants.profile.form]
parent = "Parent Tenant (Optional)"
parent_help = "Select a parent tenant if this is a subsidiary or sub-organization."
[ui.admin.tenants.parent]
local_search_placeholder = "Search tenant name or slug"
pick_tenant = "Pick tenant"
[ui.admin.tenants.registry]
title = "Tenant registry"

View File

@@ -120,6 +120,11 @@ subtitle = "현재 등록된 테넌트를 확인하고 상태를 관리합니다
[msg.admin.tenants.import_preview]
description = "tenant_id가 없는 행은 기존 테넌트 후보와 비교한 뒤 신규 생성 또는 기존 테넌트 갱신으로 처리합니다."
[msg.admin.tenants.parent]
local_picker_description = "테넌트 목록에서 상위 테넌트로 사용할 항목을 선택합니다."
local_picker_empty = "선택할 수 있는 테넌트가 없습니다."
picker_description = "org-chart에서 테넌트를 선택하면 상위 테넌트에 반영됩니다."
[msg.dev.auth]
access_denied_description = "DevFront는 관리자 전용 화면입니다. 권한이 필요하면 관리자에게 요청해 주세요."
access_denied_title = "접근 권한이 없습니다."
@@ -368,9 +373,14 @@ title = "도메인 충돌"
candidates = "후보"
confirm = "가져오기 실행"
create_new_reset = "신규 생성 (ID/slug 재설정)"
csv_parents = "CSV 상위 테넌트"
external_id = "외부 ID"
match = "매칭"
no_candidates = "후보 없음"
parent = "상위"
parent_companies = "상위 회사"
parent_company_groups = "상위 그룹사"
parent_organizations = "상위 조직"
parent_unresolved = "부모 확인 필요"
slug_exists = "slug 충돌"
title = "CSV 가져오기 확인"
@@ -699,6 +709,7 @@ remove_success = "권한이 회수되었습니다."
subtitle = "이 테넌트의 자원을 관리할 수 있는 사용자 목록입니다."
[msg.admin.tenants.create]
pick_parent_first = "상위 테넌트를 먼저 선택하세요."
subtitle = "글로벌 운영 기준의 신규 테넌트를 등록합니다."
[msg.admin.tenants.create.form]
@@ -1623,11 +1634,20 @@ domains_placeholder = "example.com, example.kr"
name = "테넌트 이름"
name_placeholder = "테넌트 이름을 입력하세요"
parent = "상위 테넌트"
pick_hanmac_parent = "한맥가족에서 선택"
pick_other_parent = "다른 테넌트 선택"
root_tenant = "최상위 테넌트로 생성"
slug = "Slug"
slug_placeholder = "tenant-slug"
status = "상태"
type = "유형"
[ui.admin.tenants.create.parent_context]
general = "일반 하위 테넌트"
hanmac = "한맥가족 하위 테넌트"
pick_required = "상위 테넌트 선택 필요"
root = "최상위 테넌트"
[ui.admin.tenants.create.memo]
title = "정책 메모"
@@ -1711,9 +1731,14 @@ view_profile = "상세 정보"
candidates = "후보"
confirm = "임포트 확정"
create_new = "새로 생성"
csv_parents = "CSV 상위 테넌트"
fixed_id = "고정 ID"
match = "매칭된 테넌트"
no_candidates = "매칭 가능한 테넌트가 없습니다."
parent = "상위"
parent_companies = "상위 회사"
parent_company_groups = "상위 그룹사"
parent_organizations = "상위 조직"
title = "임포트 미리보기"
[ui.admin.tenants.members.table]
@@ -1740,16 +1765,22 @@ allowed_domains_help = "이 도메인을 가진 이메일로 가입한 사용자
approve_button = "테넌트 승인"
description = "설명"
name = "테넌트 이름"
org_unit_type = "조직 세부타입"
slug = "슬러그 (Slug)"
status = "상태"
subtitle = "슬러그 및 상태 변경은 즉시 적용됩니다."
title = "테넌트 프로필"
type = "테넌트 유형"
visibility = "공개 범위"
[ui.admin.tenants.profile.form]
parent = "상위 테넌트 (선택)"
parent_help = "가족사 테넌트나 하위 조직을 종속시킬 경우 상위 테넌트를 선택해주세요."
[ui.admin.tenants.parent]
local_search_placeholder = "테넌트 이름 또는 슬러그 검색"
pick_tenant = "테넌트 선택"
[ui.admin.tenants.registry]
title = "Tenant registry"

View File

@@ -237,9 +237,14 @@ title = ""
candidates = ""
confirm = ""
create_new_reset = ""
csv_parents = ""
external_id = ""
match = ""
no_candidates = ""
parent = ""
parent_companies = ""
parent_company_groups = ""
parent_organizations = ""
parent_unresolved = ""
slug_exists = ""
title = ""
@@ -568,6 +573,7 @@ remove_success = ""
subtitle = ""
[msg.admin.tenants.create]
pick_parent_first = ""
subtitle = ""
[msg.admin.tenants.create.form]
@@ -1492,11 +1498,20 @@ domains_placeholder = ""
name = ""
name_placeholder = ""
parent = ""
pick_hanmac_parent = ""
pick_other_parent = ""
root_tenant = ""
slug = ""
slug_placeholder = ""
status = ""
type = ""
[ui.admin.tenants.create.parent_context]
general = ""
hanmac = ""
pick_required = ""
root = ""
[ui.admin.tenants.create.memo]
title = ""
@@ -1565,6 +1580,11 @@ seed_delete_blocked = ""
[msg.admin.tenants.import_preview]
description = ""
[msg.admin.tenants.parent]
local_picker_description = ""
local_picker_empty = ""
picker_description = ""
[msg.admin.users]
self_delete_blocked = ""
export_error = ""
@@ -1610,16 +1630,22 @@ allowed_domains_help = ""
approve_button = ""
description = ""
name = ""
org_unit_type = ""
slug = ""
status = ""
subtitle = ""
title = ""
type = ""
visibility = ""
[ui.admin.tenants.profile.form]
parent = ""
parent_help = ""
[ui.admin.tenants.parent]
local_search_placeholder = ""
pick_tenant = ""
[ui.admin.tenants.registry]
title = ""
@@ -1662,9 +1688,14 @@ tree_search_placeholder = ""
candidates = ""
confirm = ""
create_new = ""
csv_parents = ""
fixed_id = ""
match = ""
no_candidates = ""
parent = ""
parent_companies = ""
parent_company_groups = ""
parent_organizations = ""
title = ""
[ui.admin.tenants.sub.table]

View File

@@ -1,5 +1,12 @@
import { createRequire } from "node:module";
import { defineConfig, devices } from "@playwright/test";
const require = createRequire(import.meta.url);
const { shouldIncludeWebKit } =
require("../scripts/playwrightHostDeps.cjs") as {
shouldIncludeWebKit: () => boolean;
};
const configuredWorkers = process.env.PLAYWRIGHT_WORKERS
? Number.parseInt(process.env.PLAYWRIGHT_WORKERS, 10)
: undefined;
@@ -58,10 +65,14 @@ export default defineConfig({
use: { ...devices["Desktop Firefox"] },
},
{
name: "webkit",
use: { ...devices["Desktop Safari"] },
},
...(shouldIncludeWebKit()
? [
{
name: "webkit",
use: { ...devices["Desktop Safari"] },
},
]
: []),
],
/* Run your local dev server before starting the tests */

View File

@@ -35,6 +35,29 @@ if [ "${1:-}" = "--print-mode" ]; then
exit 0
fi
ensure_frontend_dependencies() {
if [ ! -f package.json ] || [ ! -f package-lock.json ]; then
return 0
fi
if command -v sha256sum >/dev/null 2>&1; then
deps_hash="$(sha256sum package.json package-lock.json | sha256sum | awk '{print $1}')"
else
deps_hash="$(cksum package.json package-lock.json | cksum | awk '{print $1}')"
fi
deps_stamp="node_modules/.baron-deps-hash"
installed_hash="$(cat "$deps_stamp" 2>/dev/null || true)"
if [ "$installed_hash" != "$deps_hash" ]; then
echo "Installing frontend dependencies from package-lock.json..."
npm ci
mkdir -p node_modules
printf '%s\n' "$deps_hash" > "$deps_stamp"
fi
}
ensure_frontend_dependencies
if [ "$mode" = "production" ]; then
echo "Running in production mode with Vite preview..."
exec sh -c "npm run build && npm run preview -- --host 0.0.0.0 --port 5175"

View File

@@ -46,9 +46,7 @@ describe("hanmac family organization order", () => {
it("does not rank generic technical centers as GPDTDC", () => {
expect(
getHanmacFamilyTenantOrderRank(
tenant("기술개발센터", "rnd-center"),
),
getHanmacFamilyTenantOrderRank(tenant("기술개발센터", "rnd-center")),
).toBe(Number.MAX_SAFE_INTEGER);
});
});

View File

@@ -51,10 +51,9 @@ function tenantToPickerNode(
tenant: TenantNode,
usersBySlug: Map<string, UserSummary[]>,
): OrgPickerTreeNode {
const tenantChildren = orderHanmacFamilyChildren(
tenant,
tenant.children,
).map((child) => tenantToPickerNode(child, usersBySlug));
const tenantChildren = orderHanmacFamilyChildren(tenant, tenant.children).map(
(child) => tenantToPickerNode(child, usersBySlug),
);
const userChildren = (usersBySlug.get(tenant.slug.toLowerCase()) || []).map(
(user) => ({
type: "user" as const,

View File

@@ -1032,12 +1032,11 @@ export function buildOrgSelectionOptions(
(familyRoot?.children ?? []).filter((node) =>
["COMPANY_GROUP", "COMPANY", "ORGANIZATION"].includes(node.type),
),
)
.map((node) => ({
descendants: collectOrgSelectionDescendants(node, 2),
id: node.id,
label: node.name,
}));
).map((node) => ({
descendants: collectOrgSelectionDescendants(node, 2),
id: node.id,
label: node.name,
}));
}
function getOrgSelectionLabel(

View File

@@ -0,0 +1,60 @@
const { execFileSync } = require("node:child_process");
const webkitHostLibraries = [
"libgtk-4.so.1",
"libgraphene-1.0.so.0",
"libxslt.so.1",
"libevent-2.1.so.7",
"libopus.so.0",
"libgstallocators-1.0.so.0",
"libgstapp-1.0.so.0",
"libgstpbutils-1.0.so.0",
"libgstaudio-1.0.so.0",
"libgsttag-1.0.so.0",
"libgstvideo-1.0.so.0",
"libgstgl-1.0.so.0",
"libgstcodecparsers-1.0.so.0",
"libgstfft-1.0.so.0",
"libflite.so.1",
"libwebpdemux.so.2",
"libavif.so.16",
"libharfbuzz-icu.so.0",
"libwebpmux.so.3",
"libwayland-server.so.0",
"libmanette-0.2.so.0",
"libenchant-2.so.2",
"libhyphen.so.0",
"libsecret-1.so.0",
"libwoff2dec.so.1.0.2",
"libx264.so",
];
function hasWebKitHostDependencies() {
if (process.platform !== "linux") {
return true;
}
let output = "";
try {
output = execFileSync("ldconfig", ["-p"], { encoding: "utf8" });
} catch {
return false;
}
return webkitHostLibraries.every((library) => output.includes(library));
}
function shouldIncludeWebKit() {
if (process.env.PLAYWRIGHT_FORCE_WEBKIT === "1") {
return true;
}
if (process.env.PLAYWRIGHT_SKIP_WEBKIT === "1") {
return false;
}
return hasWebKitHostDependencies();
}
module.exports = {
hasWebKitHostDependencies,
shouldIncludeWebKit,
};

View File

@@ -18,6 +18,8 @@ rm -rf adminfront/node_modules
tmp_dir="$(mktemp -d /tmp/baron-sso-adminfront-tests.XXXXXX)"
playwright_browsers_path="$tmp_dir/ms-playwright"
mkdir -p "$tmp_dir/scripts"
cp "$repo_root/scripts/playwrightHostDeps.cjs" "$tmp_dir/scripts/"
if command -v rsync >/dev/null 2>&1; then
rsync -rlptD --delete \
@@ -58,6 +60,53 @@ find_available_port() {
playwright_install_cmd=(npx playwright install)
playwright_install_desc="npx playwright install"
playwright_project_args=()
has_webkit_host_dependencies() {
if [ "$(uname -s)" != "Linux" ]; then
return 0
fi
if ! command -v ldconfig >/dev/null 2>&1; then
return 1
fi
local missing=0
local lib
for lib in \
libgtk-4.so.1 \
libgraphene-1.0.so.0 \
libxslt.so.1 \
libevent-2.1.so.7 \
libopus.so.0 \
libgstallocators-1.0.so.0 \
libgstapp-1.0.so.0 \
libgstpbutils-1.0.so.0 \
libgstaudio-1.0.so.0 \
libgsttag-1.0.so.0 \
libgstvideo-1.0.so.0 \
libgstgl-1.0.so.0 \
libgstcodecparsers-1.0.so.0 \
libgstfft-1.0.so.0 \
libflite.so.1 \
libwebpdemux.so.2 \
libavif.so.16 \
libharfbuzz-icu.so.0 \
libwebpmux.so.3 \
libwayland-server.so.0 \
libmanette-0.2.so.0 \
libenchant-2.so.2 \
libhyphen.so.0 \
libsecret-1.so.0 \
libwoff2dec.so.1.0.2 \
libx264.so; do
if ! ldconfig -p 2>/dev/null | grep -Fq "$lib"; then
missing=1
break
fi
done
[ "$missing" -eq 0 ]
}
if [ "$(id -u)" -eq 0 ]; then
playwright_install_cmd=(npx playwright install --with-deps)
@@ -65,6 +114,17 @@ if [ "$(id -u)" -eq 0 ]; then
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"
elif ! has_webkit_host_dependencies; then
playwright_install_cmd=(npx playwright install chromium firefox)
playwright_install_desc="npx playwright install chromium firefox"
playwright_project_args=(--project=chromium --project=firefox)
{
echo "# Adminfront WebKit Skipped"
echo
echo "- Reason: WebKit host dependencies are not installed and this user cannot run passwordless sudo."
echo "- Action: Running Chromium and Firefox projects only."
echo "- To enable WebKit locally: run \`cd adminfront && npx playwright install-deps webkit\` with sudo privileges."
} > reports/adminfront-webkit-skipped.md
fi
set +e
@@ -134,7 +194,7 @@ echo "==> adminfront using PORT=$port"
(
cd "$tmp_dir/adminfront"
PORT="$port" PLAYWRIGHT_WORKERS="${PLAYWRIGHT_WORKERS:-1}" PLAYWRIGHT_BROWSERS_PATH="$playwright_browsers_path" \
node ./node_modules/playwright/cli.js test
node ./node_modules/playwright/cli.js test "${playwright_project_args[@]}"
) 2>&1 | tee reports/adminfront-test.log
test_exit_code=${PIPESTATUS[0]}
set -e

View File

@@ -17,6 +17,14 @@ for script in \
"./devfront/scripts/runtime-mode.sh" \
"./orgfront/scripts/runtime-mode.sh"
do
if ! grep -Fq "ensure_frontend_dependencies" "$script"; then
echo "script=$script must sync frontend dependencies before start" >&2
exit 1
fi
if ! grep -Fq "package-lock.json" "$script"; then
echo "script=$script must use package-lock.json for dependency sync" >&2
exit 1
fi
assert_mode "$script" "production" "production"
assert_mode "$script" "prod" "production"
assert_mode "$script" "stage" "production"

View File

@@ -24,13 +24,19 @@ pull_compose="docker/staging_pull_compose.template.yaml"
devfront_vite="devfront/vite.config.ts"
orgfront_vite="orgfront/vite.config.ts"
adminfront_vite="adminfront/vite.config.ts"
adminfront_runtime="adminfront/scripts/runtime-mode.sh"
devfront_runtime="devfront/scripts/runtime-mode.sh"
orgfront_runtime="orgfront/scripts/runtime-mode.sh"
for file in \
"$staging_pull" \
"$pull_compose" \
"$adminfront_vite" \
"$devfront_vite" \
"$orgfront_vite"
"$orgfront_vite" \
"$adminfront_runtime" \
"$devfront_runtime" \
"$orgfront_runtime"
do
if [ ! -f "$file" ]; then
echo "missing expected file: $file" >&2
@@ -49,6 +55,7 @@ done
assert_contains "$staging_pull" 'bash scripts/render_ory_config.sh'
assert_contains "$staging_pull" 'chmod -R 777 config/.generated/ory'
assert_contains "$staging_pull" 'docker compose -f staging_pull_compose.yaml build --pull'
assert_contains "$staging_pull" 'docker compose -f staging_pull_compose.yaml up -d --remove-orphans --renew-anon-volumes'
assert_contains "$pull_compose" "baron_devfront"
assert_contains "$pull_compose" "baron_orgfront"
@@ -71,4 +78,11 @@ assert_contains "$orgfront_vite" '"sorg.hmac.kr"'
assert_contains "orgfront/biome.json" '".vite"'
assert_contains "orgfront/tsconfig.app.json" '"exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"]'
for runtime_script in "$adminfront_runtime" "$devfront_runtime" "$orgfront_runtime"; do
assert_contains "$runtime_script" "ensure_frontend_dependencies"
assert_contains "$runtime_script" "package-lock.json"
assert_contains "$runtime_script" "npm ci"
assert_contains "$runtime_script" ".baron-deps-hash"
done
echo "staging frontend deploy policy checks passed"

View File

@@ -45,10 +45,10 @@ packages:
dependency: transitive
description:
name: characters
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
version: "1.4.0"
version: "1.4.1"
cli_config:
dependency: transitive
description:
@@ -276,14 +276,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.5"
js:
dependency: transitive
description:
name: js
sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc"
url: "https://pub.dev"
source: hosted
version: "0.7.2"
leak_tracker:
dependency: transitive
description:
@@ -336,18 +328,18 @@ packages:
dependency: transitive
description:
name: matcher
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.dev"
source: hosted
version: "0.12.17"
version: "0.12.19"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
version: "0.11.1"
version: "0.13.0"
meta:
dependency: transitive
description:
@@ -669,26 +661,26 @@ packages:
dependency: transitive
description:
name: test
sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7"
sha256: "280d6d890011ca966ad08df7e8a4ddfab0fb3aa49f96ed6de56e3521347a9ae7"
url: "https://pub.dev"
source: hosted
version: "1.26.3"
version: "1.30.0"
test_api:
dependency: transitive
description:
name: test_api
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://pub.dev"
source: hosted
version: "0.7.7"
version: "0.7.10"
test_core:
dependency: transitive
description:
name: test_core
sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0"
sha256: "0381bd1585d1a924763c308100f2138205252fb90c9d4eeaf28489ee65ccde51"
url: "https://pub.dev"
source: hosted
version: "0.6.12"
version: "0.6.16"
toml:
dependency: "direct main"
description: