forked from baron/baron-sso
code-check 오류 수정
This commit is contained in:
@@ -56,6 +56,16 @@ import {
|
|||||||
} from "./orgChartPicker";
|
} from "./orgChartPicker";
|
||||||
import type { UserSchemaField } from "./userSchemaFields";
|
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 UserFormValues = UserCreateRequest & { metadata: Record<string, unknown> };
|
||||||
type UserType = "hanmac" | "external" | "personal";
|
type UserType = "hanmac" | "external" | "personal";
|
||||||
|
|
||||||
|
|||||||
@@ -79,6 +79,16 @@ import {
|
|||||||
} from "./orgChartPicker";
|
} from "./orgChartPicker";
|
||||||
import type { UserSchemaField } from "./userSchemaFields";
|
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"> & {
|
type UserFormValues = Omit<UserUpdateRequest, "metadata"> & {
|
||||||
metadata: Record<string, Record<string, string | number | boolean>>;
|
metadata: Record<string, Record<string, string | number | boolean>>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ test.describe("Tenants Management", () => {
|
|||||||
|
|
||||||
test("should export and import tenant CSV without organization/user combined import", async ({
|
test("should export and import tenant CSV without organization/user combined import", async ({
|
||||||
page,
|
page,
|
||||||
|
browserName,
|
||||||
}, testInfo) => {
|
}, testInfo) => {
|
||||||
let exportRequested = false;
|
let exportRequested = false;
|
||||||
let exportUrl = "";
|
let exportUrl = "";
|
||||||
@@ -213,9 +214,12 @@ test.describe("Tenants Management", () => {
|
|||||||
/갱신 1|Updated 1/i,
|
/갱신 1|Updated 1/i,
|
||||||
);
|
);
|
||||||
expect(importRequested).toBe(true);
|
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");
|
expect(importBody).toContain("tenant-alpha-id");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should resolve tenant CSV conflicts by choosing create and remapping parent ids", async ({
|
test("should resolve tenant CSV conflicts by choosing create and remapping parent ids", async ({
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ func TestDevHandler_Isolation(t *testing.T) {
|
|||||||
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
|
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()
|
app := fiber.New()
|
||||||
tenantA := "tenant-a"
|
tenantA := "tenant-a"
|
||||||
app.Use(func(c *fiber.Ctx) error {
|
app.Use(func(c *fiber.Ctx) error {
|
||||||
@@ -209,11 +209,11 @@ func TestDevHandler_Isolation(t *testing.T) {
|
|||||||
"client_name": "Updated Name",
|
"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 := httptest.NewRequest(http.MethodPut, "/api/v1/dev/clients/client-tenant-a", bytes.NewReader(body))
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
resp, _ := app.Test(req, -1)
|
resp, _ := app.Test(req, -1)
|
||||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
|
||||||
|
|
||||||
// Case 2: Different tenant
|
// Case 2: Different tenant
|
||||||
req = httptest.NewRequest(http.MethodPut, "/api/v1/dev/clients/client-tenant-b", bytes.NewReader(body))
|
req = httptest.NewRequest(http.MethodPut, "/api/v1/dev/clients/client-tenant-b", bytes.NewReader(body))
|
||||||
|
|||||||
@@ -348,7 +348,9 @@ function AuditLogsPage() {
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-xs">{actionLabel}</TableCell>
|
<TableCell className="text-xs">
|
||||||
|
{actionLabel}
|
||||||
|
</TableCell>
|
||||||
<TableCell className="font-mono text-xs">
|
<TableCell className="font-mono text-xs">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="break-all">{targetValue}</span>
|
<span className="break-all">{targetValue}</span>
|
||||||
@@ -401,16 +403,21 @@ function AuditLogsPage() {
|
|||||||
<div className="grid gap-3 md:grid-cols-2">
|
<div className="grid gap-3 md:grid-cols-2">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<div>
|
<div>
|
||||||
Request ID: {formatValue(details.request_id)}
|
Request ID:{" "}
|
||||||
|
{formatValue(details.request_id)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Method: {formatValue(details.method)}
|
||||||
</div>
|
</div>
|
||||||
<div>Method: {formatValue(details.method)}</div>
|
|
||||||
<div>Path: {formatValue(details.path)}</div>
|
<div>Path: {formatValue(details.path)}</div>
|
||||||
<div>
|
<div>
|
||||||
Tenant: {formatValue(details.tenant_id)}
|
Tenant: {formatValue(details.tenant_id)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1 break-all">
|
<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>After: {formatValue(details.after)}</div>
|
||||||
<div>Error: {formatValue(details.error)}</div>
|
<div>Error: {formatValue(details.error)}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -218,10 +218,12 @@ function ClientDetailsPage() {
|
|||||||
const clientSecret = hasClientSecret
|
const clientSecret = hasClientSecret
|
||||||
? client?.clientSecret || secretPlaceholder
|
? client?.clientSecret || secretPlaceholder
|
||||||
: t("ui.common.na", "N/A");
|
: t("ui.common.na", "N/A");
|
||||||
const displaySecret =
|
const displaySecret = !hasClientSecret
|
||||||
!hasClientSecret
|
? t(
|
||||||
? t("msg.dev.clients.details.secret_not_applicable", "PKCE 앱에는 Client Secret이 없습니다.")
|
"msg.dev.clients.details.secret_not_applicable",
|
||||||
: clientSecret === secretPlaceholder
|
"PKCE 앱에는 Client Secret이 없습니다.",
|
||||||
|
)
|
||||||
|
: clientSecret === secretPlaceholder
|
||||||
? t("msg.dev.clients.details.secret_unavailable", "SECRET_NOT_AVAILABLE")
|
? t("msg.dev.clients.details.secret_unavailable", "SECRET_NOT_AVAILABLE")
|
||||||
: clientSecret;
|
: clientSecret;
|
||||||
|
|
||||||
|
|||||||
@@ -463,9 +463,7 @@ function ClientGeneralPage() {
|
|||||||
(item.relation === "admins" || item.relation === "config_editor"),
|
(item.relation === "admins" || item.relation === "config_editor"),
|
||||||
) === true;
|
) === true;
|
||||||
const isGeneralSettingsReadOnly =
|
const isGeneralSettingsReadOnly =
|
||||||
!isCreate &&
|
!isCreate && relationData != null && !canEditExistingClientGeneralSettings;
|
||||||
relationData != null &&
|
|
||||||
!canEditExistingClientGeneralSettings;
|
|
||||||
const trimmedLogoUrl = logoUrl.trim();
|
const trimmedLogoUrl = logoUrl.trim();
|
||||||
const trimmedAutoLoginUrl = autoLoginUrl.trim();
|
const trimmedAutoLoginUrl = autoLoginUrl.trim();
|
||||||
const hasLogoUrl = trimmedLogoUrl.length > 0;
|
const hasLogoUrl = trimmedLogoUrl.length > 0;
|
||||||
|
|||||||
@@ -312,10 +312,13 @@ function ClientRelationsPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleInfoToggle = (event: React.MouseEvent, relation: RelationOption) => {
|
const handleInfoToggle = (
|
||||||
|
event: React.MouseEvent,
|
||||||
|
relation: RelationOption,
|
||||||
|
) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
setInfoRelation(prev => (prev === relation ? null : relation));
|
setInfoRelation((prev) => (prev === relation ? null : relation));
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!clientId) {
|
if (!clientId) {
|
||||||
@@ -401,7 +404,10 @@ function ClientRelationsPage() {
|
|||||||
<CardContent className="space-y-6">
|
<CardContent className="space-y-6">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="py-8 text-center text-sm text-muted-foreground">
|
<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>
|
</div>
|
||||||
) : isRelationshipViewForbidden ? (
|
) : isRelationshipViewForbidden ? (
|
||||||
<div className="rounded-md border border-border bg-muted/30 p-4 text-sm text-muted-foreground">
|
<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);
|
selectedUserExistingRelations.has(relation);
|
||||||
const isSelected = selectedRelations.includes(relation);
|
const isSelected = selectedRelations.includes(relation);
|
||||||
const isInfoVisible = infoRelation === relation;
|
const isInfoVisible = infoRelation === relation;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={relation} className="relative">
|
<div key={relation} className="relative">
|
||||||
<label
|
<label
|
||||||
@@ -568,7 +574,7 @@ function ClientRelationsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
{isInfoVisible && (
|
{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="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">
|
<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="space-y-1">
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
<div className="flex items-center gap-2 font-medium">
|
<div className="flex items-center gap-2 font-medium">
|
||||||
<span>{relationLabel(item.relation as RelationOption)}</span>
|
<span>
|
||||||
|
{relationLabel(item.relation as RelationOption)}
|
||||||
|
</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`rounded-full p-0.5 transition-colors ${
|
className={`rounded-full p-0.5 transition-colors ${
|
||||||
@@ -681,14 +689,21 @@ function ClientRelationsPage() {
|
|||||||
? "text-primary"
|
? "text-primary"
|
||||||
: "text-muted-foreground/60 hover: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" />
|
<Info className="h-3.5 w-3.5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{infoRelation === item.relation && (
|
{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]">
|
<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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -191,11 +191,17 @@ delete_confirm = ""
|
|||||||
delete_success = ""
|
delete_success = ""
|
||||||
empty = ""
|
empty = ""
|
||||||
fetch_error = ""
|
fetch_error = ""
|
||||||
|
import_empty = ""
|
||||||
|
import_error = ""
|
||||||
|
import_result = ""
|
||||||
missing_id = ""
|
missing_id = ""
|
||||||
not_found = ""
|
not_found = ""
|
||||||
remove_sub_confirm = ""
|
remove_sub_confirm = ""
|
||||||
subtitle = ""
|
subtitle = ""
|
||||||
|
|
||||||
|
[msg.admin.tenants.import_preview]
|
||||||
|
description = ""
|
||||||
|
|
||||||
[msg.admin.tenants.admins]
|
[msg.admin.tenants.admins]
|
||||||
add_success = ""
|
add_success = ""
|
||||||
empty = ""
|
empty = ""
|
||||||
@@ -255,9 +261,13 @@ parsed_count = ""
|
|||||||
update_success = ""
|
update_success = ""
|
||||||
|
|
||||||
[msg.admin.users.create]
|
[msg.admin.users.create]
|
||||||
|
appointment_required = ""
|
||||||
error = ""
|
error = ""
|
||||||
|
external_tenant_required = ""
|
||||||
password_required = ""
|
password_required = ""
|
||||||
|
personal_tenant_failed = ""
|
||||||
success = ""
|
success = ""
|
||||||
|
tenant_resolve_failed = ""
|
||||||
|
|
||||||
[msg.admin.users.create.account]
|
[msg.admin.users.create.account]
|
||||||
subtitle = ""
|
subtitle = ""
|
||||||
@@ -269,6 +279,7 @@ field_required = ""
|
|||||||
name_required = ""
|
name_required = ""
|
||||||
password_auto_help = ""
|
password_auto_help = ""
|
||||||
password_manual_help = ""
|
password_manual_help = ""
|
||||||
|
picker_description = ""
|
||||||
role_help = ""
|
role_help = ""
|
||||||
|
|
||||||
[msg.admin.users.create.password_generated]
|
[msg.admin.users.create.password_generated]
|
||||||
@@ -291,7 +302,9 @@ password_hint = ""
|
|||||||
[msg.admin.users.list]
|
[msg.admin.users.list]
|
||||||
delete_confirm = ""
|
delete_confirm = ""
|
||||||
empty = ""
|
empty = ""
|
||||||
|
export_error = ""
|
||||||
fetch_error = ""
|
fetch_error = ""
|
||||||
|
status_error = ""
|
||||||
subtitle = ""
|
subtitle = ""
|
||||||
|
|
||||||
[msg.admin.users.list.columns]
|
[msg.admin.users.list.columns]
|
||||||
@@ -991,6 +1004,9 @@ user = ""
|
|||||||
|
|
||||||
[ui.admin.tenants]
|
[ui.admin.tenants]
|
||||||
add = ""
|
add = ""
|
||||||
|
csv_template = ""
|
||||||
|
export = ""
|
||||||
|
import = ""
|
||||||
title = ""
|
title = ""
|
||||||
|
|
||||||
[ui.admin.tenants.admins]
|
[ui.admin.tenants.admins]
|
||||||
@@ -1120,6 +1136,15 @@ search_placeholder = ""
|
|||||||
title = ""
|
title = ""
|
||||||
tree_search_placeholder = ""
|
tree_search_placeholder = ""
|
||||||
|
|
||||||
|
[ui.admin.tenants.import_preview]
|
||||||
|
candidates = ""
|
||||||
|
confirm = ""
|
||||||
|
create_new = ""
|
||||||
|
fixed_id = ""
|
||||||
|
match = ""
|
||||||
|
no_candidates = ""
|
||||||
|
title = ""
|
||||||
|
|
||||||
[ui.admin.tenants.sub.table]
|
[ui.admin.tenants.sub.table]
|
||||||
action = ""
|
action = ""
|
||||||
name = ""
|
name = ""
|
||||||
@@ -1128,6 +1153,7 @@ status = ""
|
|||||||
|
|
||||||
[ui.admin.tenants.table]
|
[ui.admin.tenants.table]
|
||||||
actions = ""
|
actions = ""
|
||||||
|
id = ""
|
||||||
members = ""
|
members = ""
|
||||||
name = ""
|
name = ""
|
||||||
slug = ""
|
slug = ""
|
||||||
@@ -1227,6 +1253,7 @@ empty = ""
|
|||||||
fetch_error = ""
|
fetch_error = ""
|
||||||
search_placeholder = ""
|
search_placeholder = ""
|
||||||
subtitle = ""
|
subtitle = ""
|
||||||
|
toggle_status = ""
|
||||||
title = ""
|
title = ""
|
||||||
|
|
||||||
[ui.admin.users.list.breadcrumb]
|
[ui.admin.users.list.breadcrumb]
|
||||||
|
|||||||
@@ -221,6 +221,39 @@ function parseClientId(pathname: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function installDevApiMock(page: Page, state: DevApiMockState) {
|
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 = (
|
const appendAuditLog = (
|
||||||
eventType: string,
|
eventType: string,
|
||||||
action: string,
|
action: string,
|
||||||
@@ -431,6 +464,10 @@ export async function installDevApiMock(page: Page, state: DevApiMockState) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
state.clients.push(created);
|
state.clients.push(created);
|
||||||
|
if (!state.relations) {
|
||||||
|
state.relations = {};
|
||||||
|
}
|
||||||
|
state.relations[created.id] = [buildSelfConfigEditorRelation()];
|
||||||
appendAuditLog("CLIENT_CREATE", "CREATE_CLIENT", created.id);
|
appendAuditLog("CLIENT_CREATE", "CREATE_CLIENT", created.id);
|
||||||
return json(route, {
|
return json(route, {
|
||||||
client: created,
|
client: created,
|
||||||
@@ -451,7 +488,7 @@ export async function installDevApiMock(page: Page, state: DevApiMockState) {
|
|||||||
) {
|
) {
|
||||||
const clientId = pathname.split("/")[5] ?? "";
|
const clientId = pathname.split("/")[5] ?? "";
|
||||||
return json(route, {
|
return json(route, {
|
||||||
items: state.relations?.[clientId] ?? [],
|
items: await resolveClientRelations(clientId),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -156,4 +156,4 @@
|
|||||||
"authorizer": { "handler": "allow" },
|
"authorizer": { "handler": "allow" },
|
||||||
"mutators": [{ "handler": "noop" }]
|
"mutators": [{ "handler": "noop" }]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -1204,6 +1204,15 @@ title = "Tenant Members ({{count}})"
|
|||||||
total = "Total"
|
total = "Total"
|
||||||
total_label = "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]
|
[ui.admin.tenants.members.table]
|
||||||
email = "EMAIL"
|
email = "EMAIL"
|
||||||
name = "NAME"
|
name = "NAME"
|
||||||
@@ -1339,6 +1348,7 @@ name = "Name"
|
|||||||
name_placeholder = "Name Placeholder"
|
name_placeholder = "Name Placeholder"
|
||||||
password = "Password"
|
password = "Password"
|
||||||
password_placeholder = "********"
|
password_placeholder = "********"
|
||||||
|
picker_description = "Search and select a tenant."
|
||||||
phone = "Phone number"
|
phone = "Phone number"
|
||||||
phone_placeholder = "010-1234-5678"
|
phone_placeholder = "010-1234-5678"
|
||||||
position = "Position"
|
position = "Position"
|
||||||
|
|||||||
@@ -672,11 +672,17 @@ delete_confirm = "테넌트 \\\\\\\"{{name}}\\\\\\\"를 삭제할까요?"
|
|||||||
delete_success = "테넌트가 삭제되었습니다."
|
delete_success = "테넌트가 삭제되었습니다."
|
||||||
empty = "아직 등록된 테넌트가 없습니다."
|
empty = "아직 등록된 테넌트가 없습니다."
|
||||||
fetch_error = "테넌트 목록 조회에 실패했습니다."
|
fetch_error = "테넌트 목록 조회에 실패했습니다."
|
||||||
|
import_empty = "임포트 파일에 테넌트 행이 없습니다."
|
||||||
|
import_error = "테넌트 임포트에 실패했습니다: {{error}}"
|
||||||
|
import_result = "{{count}}개의 테넌트 행을 처리했습니다."
|
||||||
missing_id = "테넌트 ID가 없습니다."
|
missing_id = "테넌트 ID가 없습니다."
|
||||||
not_found = "테넌트를 찾을 수 없습니다."
|
not_found = "테넌트를 찾을 수 없습니다."
|
||||||
remove_sub_confirm = "테넌트 \\\\\\\"{{name}}\\\\\\\"을(를) 하위 조직에서 제외할까요?"
|
remove_sub_confirm = "테넌트 \\\\\\\"{{name}}\\\\\\\"을(를) 하위 조직에서 제외할까요?"
|
||||||
subtitle = "현재 등록된 테넌트를 확인하고 상태를 관리합니다."
|
subtitle = "현재 등록된 테넌트를 확인하고 상태를 관리합니다."
|
||||||
|
|
||||||
|
[msg.admin.tenants.import_preview]
|
||||||
|
description = "임포트 전에 각 행의 매칭 결과를 검토하고 처리 방식을 선택하세요."
|
||||||
|
|
||||||
[msg.admin.tenants.admins]
|
[msg.admin.tenants.admins]
|
||||||
add_success = "관리자가 추가되었습니다."
|
add_success = "관리자가 추가되었습니다."
|
||||||
empty = "등록된 관리자가 없습니다."
|
empty = "등록된 관리자가 없습니다."
|
||||||
@@ -1645,6 +1651,8 @@ delete_bulk_confirm = "선택한 {{count}}개 테넌트를 삭제할까요?"
|
|||||||
|
|
||||||
[msg.admin.users]
|
[msg.admin.users]
|
||||||
self_delete_blocked = "자신의 계정은 삭제할 수 없습니다."
|
self_delete_blocked = "자신의 계정은 삭제할 수 없습니다."
|
||||||
|
export_error = "사용자 내보내기에 실패했습니다: {{error}}"
|
||||||
|
status_error = "사용자 상태 변경에 실패했습니다: {{error}}"
|
||||||
|
|
||||||
[ui.admin.apikeys.registry]
|
[ui.admin.apikeys.registry]
|
||||||
title = "API Key Registry"
|
title = "API Key Registry"
|
||||||
@@ -1658,6 +1666,15 @@ title = "테넌트 구성원 ({{count}})"
|
|||||||
total = "전체"
|
total = "전체"
|
||||||
total_label = "전체"
|
total_label = "전체"
|
||||||
|
|
||||||
|
[ui.admin.tenants.import_preview]
|
||||||
|
candidates = "후보"
|
||||||
|
confirm = "임포트 확정"
|
||||||
|
create_new = "새로 생성"
|
||||||
|
fixed_id = "고정 ID"
|
||||||
|
match = "매칭된 테넌트"
|
||||||
|
no_candidates = "매칭 가능한 테넌트가 없습니다."
|
||||||
|
title = "임포트 미리보기"
|
||||||
|
|
||||||
[ui.admin.tenants.members.table]
|
[ui.admin.tenants.members.table]
|
||||||
email = "EMAIL"
|
email = "EMAIL"
|
||||||
name = "NAME"
|
name = "NAME"
|
||||||
@@ -1793,6 +1810,7 @@ name = "이름"
|
|||||||
name_placeholder = "홍길동"
|
name_placeholder = "홍길동"
|
||||||
password = "비밀번호"
|
password = "비밀번호"
|
||||||
password_placeholder = "********"
|
password_placeholder = "********"
|
||||||
|
picker_description = "배정할 테넌트를 검색해서 선택하세요."
|
||||||
phone = "전화번호"
|
phone = "전화번호"
|
||||||
phone_placeholder = "010-1234-5678"
|
phone_placeholder = "010-1234-5678"
|
||||||
position = "직급"
|
position = "직급"
|
||||||
|
|||||||
@@ -1517,9 +1517,17 @@ import_partial_success = ""
|
|||||||
|
|
||||||
[msg.admin.tenants]
|
[msg.admin.tenants]
|
||||||
delete_bulk_confirm = ""
|
delete_bulk_confirm = ""
|
||||||
|
import_empty = ""
|
||||||
|
import_error = ""
|
||||||
|
import_result = ""
|
||||||
|
|
||||||
|
[msg.admin.tenants.import_preview]
|
||||||
|
description = ""
|
||||||
|
|
||||||
[msg.admin.users]
|
[msg.admin.users]
|
||||||
self_delete_blocked = ""
|
self_delete_blocked = ""
|
||||||
|
export_error = ""
|
||||||
|
status_error = ""
|
||||||
|
|
||||||
[ui.admin.apikeys.registry]
|
[ui.admin.apikeys.registry]
|
||||||
title = ""
|
title = ""
|
||||||
@@ -1604,6 +1612,15 @@ search_placeholder = ""
|
|||||||
title = ""
|
title = ""
|
||||||
tree_search_placeholder = ""
|
tree_search_placeholder = ""
|
||||||
|
|
||||||
|
[ui.admin.tenants.import_preview]
|
||||||
|
candidates = ""
|
||||||
|
confirm = ""
|
||||||
|
create_new = ""
|
||||||
|
fixed_id = ""
|
||||||
|
match = ""
|
||||||
|
no_candidates = ""
|
||||||
|
title = ""
|
||||||
|
|
||||||
[ui.admin.tenants.sub.table]
|
[ui.admin.tenants.sub.table]
|
||||||
action = ""
|
action = ""
|
||||||
name = ""
|
name = ""
|
||||||
@@ -1668,6 +1685,7 @@ name = ""
|
|||||||
name_placeholder = ""
|
name_placeholder = ""
|
||||||
password = ""
|
password = ""
|
||||||
password_placeholder = ""
|
password_placeholder = ""
|
||||||
|
picker_description = ""
|
||||||
phone = ""
|
phone = ""
|
||||||
phone_placeholder = ""
|
phone_placeholder = ""
|
||||||
position = ""
|
position = ""
|
||||||
|
|||||||
@@ -6,8 +6,40 @@ job_name="${1:-adminfront-tests}"
|
|||||||
mkdir -p reports
|
mkdir -p reports
|
||||||
rm -rf adminfront/node_modules
|
rm -rf adminfront/node_modules
|
||||||
|
|
||||||
playwright_install_cmd=(npx playwright install --with-deps)
|
is_port_available() {
|
||||||
playwright_install_desc="npx playwright install --with-deps"
|
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
|
set +e
|
||||||
(
|
(
|
||||||
@@ -67,6 +99,11 @@ fi
|
|||||||
|
|
||||||
set +e
|
set +e
|
||||||
port="${PORT:-5180}"
|
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"
|
echo "==> adminfront using PORT=$port"
|
||||||
(
|
(
|
||||||
cd adminfront
|
cd adminfront
|
||||||
|
|||||||
Reference in New Issue
Block a user