forked from baron/baron-sso
dev 브런치 반영 code-check 오류 수정
This commit is contained in:
@@ -3,6 +3,10 @@ import { defineConfig, devices } from "@playwright/test";
|
|||||||
const configuredWorkers = process.env.PLAYWRIGHT_WORKERS
|
const configuredWorkers = process.env.PLAYWRIGHT_WORKERS
|
||||||
? Number.parseInt(process.env.PLAYWRIGHT_WORKERS, 10)
|
? Number.parseInt(process.env.PLAYWRIGHT_WORKERS, 10)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
const port = Number.parseInt(process.env.PORT ?? "5173", 10);
|
||||||
|
const defaultBaseUrl = `http://127.0.0.1:${port}`;
|
||||||
|
const baseURL = process.env.BASE_URL ?? defaultBaseUrl;
|
||||||
|
const reuseExistingServer = !process.env.CI && !process.env.PORT;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read environment variables from file.
|
* Read environment variables from file.
|
||||||
@@ -34,7 +38,7 @@ export default defineConfig({
|
|||||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||||
use: {
|
use: {
|
||||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||||
baseURL: "http://localhost:5173",
|
baseURL,
|
||||||
|
|
||||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||||
trace: "retain-on-failure",
|
trace: "retain-on-failure",
|
||||||
@@ -60,12 +64,14 @@ export default defineConfig({
|
|||||||
],
|
],
|
||||||
|
|
||||||
/* Run your local dev server before starting the tests */
|
/* Run your local dev server before starting the tests */
|
||||||
webServer: {
|
webServer: process.env.BASE_URL
|
||||||
|
? undefined
|
||||||
|
: {
|
||||||
command: process.env.CI
|
command: process.env.CI
|
||||||
? "npm run build && npm run preview -- --port 5173"
|
? `npm run build && npm run preview -- --host 127.0.0.1 --port ${port}`
|
||||||
: "npm run dev",
|
: `npm run dev -- --host 127.0.0.1 --port ${port}`,
|
||||||
url: "http://localhost:5173",
|
url: defaultBaseUrl,
|
||||||
reuseExistingServer: !process.env.CI,
|
reuseExistingServer,
|
||||||
timeout: 120 * 1000,
|
timeout: 120 * 1000,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -134,27 +134,23 @@ const SidebarNode: React.FC<{
|
|||||||
? "bg-primary text-primary-foreground font-semibold"
|
? "bg-primary text-primary-foreground font-semibold"
|
||||||
: "hover:bg-muted/60 text-muted-foreground hover:text-foreground"
|
: "hover:bg-muted/60 text-muted-foreground hover:text-foreground"
|
||||||
} ${isMatching ? "ring-1 ring-primary/30 bg-primary/5" : ""}`}
|
} ${isMatching ? "ring-1 ring-primary/30 bg-primary/5" : ""}`}
|
||||||
onClick={() => onSelect(node.id)}
|
onClick={() => {
|
||||||
|
onSelect(node.id);
|
||||||
|
if (hasChildren) setIsExpanded(!isExpanded);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-center flex-1 min-w-0">
|
<div className="flex items-center flex-1 min-w-0">
|
||||||
{/* Indent & Expander */}
|
{/* Indent & Expander */}
|
||||||
<div style={{ width: `${level * 1.2}rem` }} className="shrink-0" />
|
<div style={{ width: `${level * 1.2}rem` }} className="shrink-0" />
|
||||||
<div className="w-5 h-5 flex items-center justify-center mr-1">
|
<div className="w-5 h-5 flex items-center justify-center mr-1">
|
||||||
{hasChildren ? (
|
{hasChildren ? (
|
||||||
<button
|
<span className="rounded p-0.5">
|
||||||
type="button"
|
|
||||||
className="hover:bg-primary/20 rounded p-0.5"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setIsExpanded(!isExpanded);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isExpanded ? (
|
{isExpanded ? (
|
||||||
<ChevronDown size={14} />
|
<ChevronDown size={14} />
|
||||||
) : (
|
) : (
|
||||||
<ChevronRight size={14} />
|
<ChevronRight size={14} />
|
||||||
)}
|
)}
|
||||||
</button>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
level > 0 && <div className="w-1 h-1 rounded-full bg-border" />
|
level > 0 && <div className="w-1 h-1 rounded-full bg-border" />
|
||||||
)}
|
)}
|
||||||
@@ -457,7 +453,7 @@ function TenantUserGroupsTab() {
|
|||||||
{selectedNode.slug}
|
{selectedNode.slug}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<CardDescription className="flex items-center gap-2 mt-0.5">
|
<div className="text-sm text-muted-foreground flex items-center gap-2 mt-0.5">
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<Users size={12} /> {selectedNode.recursiveMemberCount}{" "}
|
<Users size={12} /> {selectedNode.recursiveMemberCount}{" "}
|
||||||
{t("ui.admin.tenants.table.members", "명")}
|
{t("ui.admin.tenants.table.members", "명")}
|
||||||
@@ -469,7 +465,7 @@ function TenantUserGroupsTab() {
|
|||||||
selectedNode.type,
|
selectedNode.type,
|
||||||
)}
|
)}
|
||||||
</Badge>
|
</Badge>
|
||||||
</CardDescription>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ test.describe("Bulk Actions and Tree Search", () => {
|
|||||||
await searchInput.fill("Eng");
|
await searchInput.fill("Eng");
|
||||||
|
|
||||||
const engNode = page
|
const engNode = page
|
||||||
.locator("button")
|
.locator('button, [role="button"]')
|
||||||
.filter({ hasText: "Engineering" })
|
.filter({ hasText: "Engineering" })
|
||||||
.first();
|
.first();
|
||||||
await expect(engNode).toBeVisible();
|
await expect(engNode).toBeVisible();
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"baron-sso-backend/internal/domain"
|
||||||
|
"baron-sso-backend/internal/service"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"baron-sso-backend/internal/domain"
|
|
||||||
"baron-sso-backend/internal/service"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -44,4 +44,4 @@ func migrateSchemas(db *gorm.DB) error {
|
|||||||
&domain.SharedLink{},
|
&domain.SharedLink{},
|
||||||
// &domain.RelyingParty{}, // Removed: SSOT is Hydra + Keto
|
// &domain.RelyingParty{}, // Removed: SSOT is Hydra + Keto
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1519,7 +1519,7 @@ func TestRevokeHeadlessJWKSCache_DeletesCachedState(t *testing.T) {
|
|||||||
assert.Nil(t, stored)
|
assert.Nil(t, stored)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestListAuditLogs_TenantMemberForbidden(t *testing.T) {
|
func TestListAuditLogs_TenantMemberWithoutAuditPermissionReturnsEmpty(t *testing.T) {
|
||||||
h := &DevHandler{
|
h := &DevHandler{
|
||||||
Hydra: &service.HydraAdminService{AdminURL: "http://hydra.test"},
|
Hydra: &service.HydraAdminService{AdminURL: "http://hydra.test"},
|
||||||
AuditRepo: &mockAuditRepo{},
|
AuditRepo: &mockAuditRepo{},
|
||||||
@@ -1540,7 +1540,11 @@ func TestListAuditLogs_TenantMemberForbidden(t *testing.T) {
|
|||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/dev/audit-logs", nil)
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/dev/audit-logs", nil)
|
||||||
resp, _ := app.Test(req, -1)
|
resp, _ := app.Test(req, -1)
|
||||||
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
|
var result devAuditListResponse
|
||||||
|
_ = json.NewDecoder(resp.Body).Decode(&result)
|
||||||
|
assert.Empty(t, result.Items)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestListAuditLogs_RPAdminScope(t *testing.T) {
|
func TestListAuditLogs_RPAdminScope(t *testing.T) {
|
||||||
@@ -1915,6 +1919,20 @@ func TestRemoveClientRelation_RPAdminAllowedByManagePermission(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSearchUsers_RPAdminSearchByNameOrEmailWithinTenantScope(t *testing.T) {
|
func TestSearchUsers_RPAdminSearchByNameOrEmailWithinTenantScope(t *testing.T) {
|
||||||
|
transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||||
|
if r.Method == http.MethodGet && r.URL.Path == "/clients/client-1" {
|
||||||
|
return httpJSONAny(r, http.StatusOK, map[string]any{
|
||||||
|
"client_id": "client-1",
|
||||||
|
"client_name": "App One",
|
||||||
|
"metadata": map[string]any{
|
||||||
|
"tenant_id": "tenant-1",
|
||||||
|
"status": "active",
|
||||||
|
},
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
return httpJSONAny(r, http.StatusNotFound, nil), nil
|
||||||
|
})
|
||||||
|
|
||||||
mockKratos := new(devMockKratosAdmin)
|
mockKratos := new(devMockKratosAdmin)
|
||||||
mockKratos.On("ListIdentities", mock.Anything).Return([]service.KratosIdentity{
|
mockKratos.On("ListIdentities", mock.Anything).Return([]service.KratosIdentity{
|
||||||
{
|
{
|
||||||
@@ -1938,6 +1956,10 @@ func TestSearchUsers_RPAdminSearchByNameOrEmailWithinTenantScope(t *testing.T) {
|
|||||||
}, nil)
|
}, nil)
|
||||||
|
|
||||||
h := &DevHandler{
|
h := &DevHandler{
|
||||||
|
Hydra: &service.HydraAdminService{
|
||||||
|
AdminURL: "http://hydra.test",
|
||||||
|
HTTPClient: &http.Client{Transport: transport},
|
||||||
|
},
|
||||||
KratosAdmin: mockKratos,
|
KratosAdmin: mockKratos,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1951,21 +1973,25 @@ func TestSearchUsers_RPAdminSearchByNameOrEmailWithinTenantScope(t *testing.T) {
|
|||||||
ManageableTenants: []domain.Tenant{
|
ManageableTenants: []domain.Tenant{
|
||||||
{ID: "tenant-1", Slug: "tenant-one"},
|
{ID: "tenant-1", Slug: "tenant-one"},
|
||||||
},
|
},
|
||||||
|
Metadata: map[string]any{
|
||||||
|
"managed_client_ids": []any{"client-1"},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
return c.Next()
|
return c.Next()
|
||||||
})
|
})
|
||||||
app.Get("/api/v1/dev/users", h.SearchUsers)
|
app.Get("/api/v1/dev/users", h.SearchUsers)
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/dev/users?search=alice", nil)
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/dev/users?clientId=client-1&search=alice", nil)
|
||||||
resp, _ := app.Test(req, -1)
|
resp, _ := app.Test(req, -1)
|
||||||
|
|
||||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
var result devUserListResponse
|
var result devUserListResponse
|
||||||
_ = json.NewDecoder(resp.Body).Decode(&result)
|
_ = json.NewDecoder(resp.Body).Decode(&result)
|
||||||
assert.Len(t, result.Items, 1)
|
if assert.Len(t, result.Items, 1) {
|
||||||
assert.Equal(t, "user-1", result.Items[0].ID)
|
assert.Equal(t, "user-1", result.Items[0].ID)
|
||||||
assert.Equal(t, "Alice Kim", result.Items[0].Name)
|
assert.Equal(t, "Alice Kim", result.Items[0].Name)
|
||||||
assert.Equal(t, "alice@example.com", result.Items[0].Email)
|
assert.Equal(t, "alice@example.com", result.Items[0].Email)
|
||||||
|
}
|
||||||
mockKratos.AssertExpectations(t)
|
mockKratos.AssertExpectations(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -868,7 +868,6 @@ func normalizeTenantType(value string) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func (h *TenantHandler) CreateShareLink(c *fiber.Ctx) error {
|
func (h *TenantHandler) CreateShareLink(c *fiber.Ctx) error {
|
||||||
tenantID := c.Params("id")
|
tenantID := c.Params("id")
|
||||||
var req struct {
|
var req struct {
|
||||||
@@ -932,7 +931,9 @@ func (h *TenantHandler) GetPublicOrgChart(c *fiber.Ctx) error {
|
|||||||
curr := id
|
curr := id
|
||||||
for {
|
for {
|
||||||
p, exists := parentMap[curr]
|
p, exists := parentMap[curr]
|
||||||
if !exists || p == "" { break }
|
if !exists || p == "" {
|
||||||
|
break
|
||||||
|
}
|
||||||
curr = p
|
curr = p
|
||||||
}
|
}
|
||||||
return curr
|
return curr
|
||||||
@@ -967,10 +968,14 @@ func (h *TenantHandler) GetPublicOrgChart(c *fiber.Ctx) error {
|
|||||||
var usersByID []domain.User
|
var usersByID []domain.User
|
||||||
h.DB.Where("tenant_id IN ?", tenantIDs).Preload("Tenant").Find(&usersByID)
|
h.DB.Where("tenant_id IN ?", tenantIDs).Preload("Tenant").Find(&usersByID)
|
||||||
for _, u := range usersByID {
|
for _, u := range usersByID {
|
||||||
if u.Status != "active" || seen[u.ID] { continue }
|
if u.Status != "active" || seen[u.ID] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
seen[u.ID] = true
|
seen[u.ID] = true
|
||||||
cc := u.CompanyCode
|
cc := u.CompanyCode
|
||||||
if cc == "" && u.Tenant != nil { cc = u.Tenant.Slug }
|
if cc == "" && u.Tenant != nil {
|
||||||
|
cc = u.Tenant.Slug
|
||||||
|
}
|
||||||
publicUsers = append(publicUsers, publicUserSummary{
|
publicUsers = append(publicUsers, publicUserSummary{
|
||||||
ID: u.ID, Name: u.Name, Position: u.Position, JobTitle: u.JobTitle, CompanyCode: cc, Status: u.Status,
|
ID: u.ID, Name: u.Name, Position: u.Position, JobTitle: u.JobTitle, CompanyCode: cc, Status: u.Status,
|
||||||
})
|
})
|
||||||
@@ -980,10 +985,14 @@ func (h *TenantHandler) GetPublicOrgChart(c *fiber.Ctx) error {
|
|||||||
var usersBySlug []domain.User
|
var usersBySlug []domain.User
|
||||||
h.DB.Where("company_code IN ?", slugs).Preload("Tenant").Find(&usersBySlug)
|
h.DB.Where("company_code IN ?", slugs).Preload("Tenant").Find(&usersBySlug)
|
||||||
for _, u := range usersBySlug {
|
for _, u := range usersBySlug {
|
||||||
if u.Status != "active" || seen[u.ID] { continue }
|
if u.Status != "active" || seen[u.ID] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
seen[u.ID] = true
|
seen[u.ID] = true
|
||||||
cc := u.CompanyCode
|
cc := u.CompanyCode
|
||||||
if cc == "" && u.Tenant != nil { cc = u.Tenant.Slug }
|
if cc == "" && u.Tenant != nil {
|
||||||
|
cc = u.Tenant.Slug
|
||||||
|
}
|
||||||
publicUsers = append(publicUsers, publicUserSummary{
|
publicUsers = append(publicUsers, publicUserSummary{
|
||||||
ID: u.ID, Name: u.Name, Position: u.Position, JobTitle: u.JobTitle, CompanyCode: cc, Status: u.Status,
|
ID: u.ID, Name: u.Name, Position: u.Position, JobTitle: u.JobTitle, CompanyCode: cc, Status: u.Status,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -263,6 +263,7 @@ func (m *MockTenantService) DeleteTenantsBulk(ctx context.Context, tenantIDs []s
|
|||||||
args := m.Called(ctx, tenantIDs)
|
args := m.Called(ctx, tenantIDs)
|
||||||
return args.Error(0)
|
return args.Error(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockTenantService) ListJoinedTenants(ctx context.Context, userID string) ([]domain.Tenant, error) {
|
func (m *MockTenantService) ListJoinedTenants(ctx context.Context, userID string) ([]domain.Tenant, error) {
|
||||||
args := m.Called(ctx, userID)
|
args := m.Called(ctx, userID)
|
||||||
if args.Get(0) != nil {
|
if args.Get(0) != nil {
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ func (m *MockKratosAdminServiceShared) GetIdentity(ctx context.Context, identity
|
|||||||
}
|
}
|
||||||
return args.Get(0).(*KratosIdentity), args.Error(1)
|
return args.Get(0).(*KratosIdentity), args.Error(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockKratosAdminServiceShared) UpdateIdentity(ctx context.Context, identityID string, traits map[string]interface{}, state string) (*KratosIdentity, error) {
|
func (m *MockKratosAdminServiceShared) UpdateIdentity(ctx context.Context, identityID string, traits map[string]interface{}, state string) (*KratosIdentity, error) {
|
||||||
args := m.Called(ctx, identityID, traits, state)
|
args := m.Called(ctx, identityID, traits, state)
|
||||||
if args.Get(0) == nil {
|
if args.Get(0) == nil {
|
||||||
@@ -120,9 +121,11 @@ func (m *MockKratosAdminServiceShared) CreateUser(ctx context.Context, user *dom
|
|||||||
func (m *MockKratosAdminServiceShared) ListIdentitySessions(ctx context.Context, identityID string) ([]KratosSession, error) {
|
func (m *MockKratosAdminServiceShared) ListIdentitySessions(ctx context.Context, identityID string) ([]KratosSession, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockKratosAdminServiceShared) GetSession(ctx context.Context, sessionID string) (*KratosSession, error) {
|
func (m *MockKratosAdminServiceShared) GetSession(ctx context.Context, sessionID string) (*KratosSession, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockKratosAdminServiceShared) DeleteSession(ctx context.Context, sessionID string) error {
|
func (m *MockKratosAdminServiceShared) DeleteSession(ctx context.Context, sessionID string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,8 +19,10 @@ import (
|
|||||||
"github.com/xuri/excelize/v2"
|
"github.com/xuri/excelize/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var whitespaceRegex = regexp.MustCompile(`\s+`)
|
var (
|
||||||
var nonAlphaNumRegex = regexp.MustCompile(`[^a-zA-Z0-9가-힣]+`)
|
whitespaceRegex = regexp.MustCompile(`\s+`)
|
||||||
|
nonAlphaNumRegex = regexp.MustCompile(`[^a-zA-Z0-9가-힣]+`)
|
||||||
|
)
|
||||||
|
|
||||||
type ProgressData struct {
|
type ProgressData struct {
|
||||||
Current int `json:"current"`
|
Current int `json:"current"`
|
||||||
@@ -102,11 +104,15 @@ func (s *orgChartService) ImportOrgChart(ctx context.Context, tenantID string, r
|
|||||||
|
|
||||||
for sheetIdx, records := range allSheetsRecords {
|
for sheetIdx, records := range allSheetsRecords {
|
||||||
for i, row := range records {
|
for i, row := range records {
|
||||||
if len(row) < 2 { continue }
|
if len(row) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
tempMap := make(map[string]int)
|
tempMap := make(map[string]int)
|
||||||
for j, cell := range row {
|
for j, cell := range row {
|
||||||
clean := s.cleanHeader(cell)
|
clean := s.cleanHeader(cell)
|
||||||
if clean != "" { tempMap[clean] = j }
|
if clean != "" {
|
||||||
|
tempMap[clean] = j
|
||||||
|
}
|
||||||
}
|
}
|
||||||
emailIdx := s.findBestMatch(tempMap, fieldMapping["email"])
|
emailIdx := s.findBestMatch(tempMap, fieldMapping["email"])
|
||||||
nameIdx := s.findBestMatch(tempMap, fieldMapping["name"])
|
nameIdx := s.findBestMatch(tempMap, fieldMapping["name"])
|
||||||
@@ -114,7 +120,8 @@ func (s *orgChartService) ImportOrgChart(ctx context.Context, tenantID string, r
|
|||||||
for j, cell := range row {
|
for j, cell := range row {
|
||||||
c := s.cleanHeader(cell)
|
c := s.cleanHeader(cell)
|
||||||
if strings.Contains(c, "mail") || strings.Contains(c, "계정") || strings.Contains(c, "id") || strings.Contains(c, "사번") {
|
if strings.Contains(c, "mail") || strings.Contains(c, "계정") || strings.Contains(c, "id") || strings.Contains(c, "사번") {
|
||||||
emailIdx = j; break
|
emailIdx = j
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -124,13 +131,17 @@ func (s *orgChartService) ImportOrgChart(ctx context.Context, tenantID string, r
|
|||||||
for key, aliases := range fieldMapping {
|
for key, aliases := range fieldMapping {
|
||||||
actualMap[key] = s.findBestMatch(tempMap, aliases)
|
actualMap[key] = s.findBestMatch(tempMap, aliases)
|
||||||
}
|
}
|
||||||
if actualMap["email"] == -1 { actualMap["email"] = emailIdx }
|
if actualMap["email"] == -1 {
|
||||||
|
actualMap["email"] = emailIdx
|
||||||
|
}
|
||||||
found = true
|
found = true
|
||||||
slog.Info("Found header row", "sheet", sheetIdx, "row", i)
|
slog.Info("Found header row", "sheet", sheetIdx, "row", i)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if found { break }
|
if found {
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !found {
|
if !found {
|
||||||
@@ -173,17 +184,23 @@ func (s *orgChartService) ImportOrgChart(ctx context.Context, tenantID string, r
|
|||||||
}
|
}
|
||||||
|
|
||||||
for rowIdx, record := range dataRows {
|
for rowIdx, record := range dataRows {
|
||||||
if len(record) == 0 { continue }
|
if len(record) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
email := s.getVal(record, actualMap["email"])
|
email := s.getVal(record, actualMap["email"])
|
||||||
name := s.getVal(record, actualMap["name"])
|
name := s.getVal(record, actualMap["name"])
|
||||||
if email == "" || name == "" { continue }
|
if email == "" || name == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
position := s.getVal(record, actualMap["position"])
|
position := s.getVal(record, actualMap["position"])
|
||||||
jobTitle := s.getVal(record, actualMap["jobtitle"])
|
jobTitle := s.getVal(record, actualMap["jobtitle"])
|
||||||
phone := s.normalizePhone(s.getVal(record, actualMap["phone"]))
|
phone := s.normalizePhone(s.getVal(record, actualMap["phone"]))
|
||||||
|
|
||||||
companyName := s.getVal(record, actualMap["company"])
|
companyName := s.getVal(record, actualMap["company"])
|
||||||
if companyName == "" { companyName = "Main" }
|
if companyName == "" {
|
||||||
|
companyName = "Main"
|
||||||
|
}
|
||||||
companySlug := s.generateCompanySlug(companyName)
|
companySlug := s.generateCompanySlug(companyName)
|
||||||
|
|
||||||
companyTenantID, err := s.ensureCompanyTenant(ctx, tenantID, companyName, companySlug, email, pathCache, result)
|
companyTenantID, err := s.ensureCompanyTenant(ctx, tenantID, companyName, companySlug, email, pathCache, result)
|
||||||
@@ -315,19 +332,25 @@ func (s *orgChartService) cleanHeader(val string) string {
|
|||||||
func (s *orgChartService) findBestMatch(tempMap map[string]int, aliases []string) int {
|
func (s *orgChartService) findBestMatch(tempMap map[string]int, aliases []string) int {
|
||||||
for _, alias := range aliases {
|
for _, alias := range aliases {
|
||||||
ca := s.cleanHeader(alias)
|
ca := s.cleanHeader(alias)
|
||||||
if idx, ok := tempMap[ca]; ok { return idx }
|
if idx, ok := tempMap[ca]; ok {
|
||||||
|
return idx
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for cleaned, idx := range tempMap {
|
for cleaned, idx := range tempMap {
|
||||||
for _, alias := range aliases {
|
for _, alias := range aliases {
|
||||||
ca := s.cleanHeader(alias)
|
ca := s.cleanHeader(alias)
|
||||||
if len(ca) >= 2 && (strings.Contains(cleaned, ca) || strings.Contains(ca, cleaned)) { return idx }
|
if len(ca) >= 2 && (strings.Contains(cleaned, ca) || strings.Contains(ca, cleaned)) {
|
||||||
|
return idx
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *orgChartService) getVal(record []string, idx int) string {
|
func (s *orgChartService) getVal(record []string, idx int) string {
|
||||||
if idx == -1 || idx >= len(record) { return "" }
|
if idx == -1 || idx >= len(record) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
return strings.TrimSpace(record[idx])
|
return strings.TrimSpace(record[idx])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -360,7 +383,9 @@ func (s *orgChartService) normalizePhone(phone string) string {
|
|||||||
|
|
||||||
func (s *orgChartService) readCSV(r io.Reader) ([][]string, error) {
|
func (s *orgChartService) readCSV(r io.Reader) ([][]string, error) {
|
||||||
data, err := io.ReadAll(r)
|
data, err := io.ReadAll(r)
|
||||||
if err != nil { return nil, err }
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
reader := csv.NewReader(bytes.NewReader(bytes.TrimPrefix(data, []byte("\xef\xbb\xbf"))))
|
reader := csv.NewReader(bytes.NewReader(bytes.TrimPrefix(data, []byte("\xef\xbb\xbf"))))
|
||||||
reader.LazyQuotes = true
|
reader.LazyQuotes = true
|
||||||
reader.FieldsPerRecord = -1
|
reader.FieldsPerRecord = -1
|
||||||
@@ -369,11 +394,15 @@ func (s *orgChartService) readCSV(r io.Reader) ([][]string, error) {
|
|||||||
|
|
||||||
func (s *orgChartService) readAllXLSXSheets(r io.Reader) ([][][]string, error) {
|
func (s *orgChartService) readAllXLSXSheets(r io.Reader) ([][][]string, error) {
|
||||||
f, err := excelize.OpenReader(r)
|
f, err := excelize.OpenReader(r)
|
||||||
if err != nil { return nil, err }
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
var allRecords [][][]string
|
var allRecords [][][]string
|
||||||
for _, sheet := range f.GetSheetList() {
|
for _, sheet := range f.GetSheetList() {
|
||||||
if rows, err := f.GetRows(sheet); err == nil { allRecords = append(allRecords, rows) }
|
if rows, err := f.GetRows(sheet); err == nil {
|
||||||
|
allRecords = append(allRecords, rows)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return allRecords, nil
|
return allRecords, nil
|
||||||
}
|
}
|
||||||
@@ -385,14 +414,18 @@ func (s *orgChartService) generateCompanySlug(name string) string {
|
|||||||
"ptc": "ptc", "피티씨": "ptc", "바론": "baron", "한라": "halla",
|
"ptc": "ptc", "피티씨": "ptc", "바론": "baron", "한라": "halla",
|
||||||
}
|
}
|
||||||
for k, v := range slugs {
|
for k, v := range slugs {
|
||||||
if strings.Contains(n, k) || strings.Contains(n, v) { return v }
|
if strings.Contains(n, k) || strings.Contains(n, v) {
|
||||||
|
return v
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return utils.GenerateSlug(name)
|
return utils.GenerateSlug(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func isAlphaNumeric(s string) bool {
|
func isAlphaNumeric(s string) bool {
|
||||||
for _, r := range s {
|
for _, r := range s {
|
||||||
if (r < 'a' || r > 'z') && (r < '0' || r > '9') && r != '-' { return false }
|
if (r < 'a' || r > 'z') && (r < '0' || r > '9') && r != '-' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -411,7 +444,9 @@ func (s *orgChartService) ensureCompanyTenant(ctx context.Context, rootID, name,
|
|||||||
}
|
}
|
||||||
|
|
||||||
cacheKey := "company:" + slug
|
cacheKey := "company:" + slug
|
||||||
if id, ok := cache[cacheKey]; ok { return id, nil }
|
if id, ok := cache[cacheKey]; ok {
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
|
||||||
tenant, _ := s.tenantRepo.FindBySlug(ctx, slug)
|
tenant, _ := s.tenantRepo.FindBySlug(ctx, slug)
|
||||||
if tenant == nil {
|
if tenant == nil {
|
||||||
@@ -420,7 +455,9 @@ func (s *orgChartService) ensureCompanyTenant(ctx context.Context, rootID, name,
|
|||||||
|
|
||||||
if tenant == nil {
|
if tenant == nil {
|
||||||
tenant = &domain.Tenant{ID: uuid.NewString(), Name: name, Slug: slug, Type: domain.TenantTypeCompany, Status: domain.TenantStatusActive, ParentID: &rootID}
|
tenant = &domain.Tenant{ID: uuid.NewString(), Name: name, Slug: slug, Type: domain.TenantTypeCompany, Status: domain.TenantStatusActive, ParentID: &rootID}
|
||||||
if err := s.tenantRepo.Create(ctx, tenant); err != nil { return "", err }
|
if err := s.tenantRepo.Create(ctx, tenant); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
if s.ketoOutboxRepo != nil {
|
if s.ketoOutboxRepo != nil {
|
||||||
_ = s.ketoOutboxRepo.Create(ctx, &domain.KetoOutbox{Namespace: "Tenant", Object: tenant.ID, Relation: "parents", Subject: "Tenant:" + rootID, Action: domain.KetoOutboxActionCreate})
|
_ = s.ketoOutboxRepo.Create(ctx, &domain.KetoOutbox{Namespace: "Tenant", Object: tenant.ID, Relation: "parents", Subject: "Tenant:" + rootID, Action: domain.KetoOutboxActionCreate})
|
||||||
}
|
}
|
||||||
@@ -428,8 +465,12 @@ func (s *orgChartService) ensureCompanyTenant(ctx context.Context, rootID, name,
|
|||||||
}
|
}
|
||||||
|
|
||||||
domainPart := ""
|
domainPart := ""
|
||||||
if parts := strings.Split(email, "@"); len(parts) == 2 { domainPart = parts[1] }
|
if parts := strings.Split(email, "@"); len(parts) == 2 {
|
||||||
if domainPart != "" { _ = s.tenantRepo.AddDomain(ctx, tenant.ID, domainPart, true) }
|
domainPart = parts[1]
|
||||||
|
}
|
||||||
|
if domainPart != "" {
|
||||||
|
_ = s.tenantRepo.AddDomain(ctx, tenant.ID, domainPart, true)
|
||||||
|
}
|
||||||
|
|
||||||
cache[cacheKey] = tenant.ID
|
cache[cacheKey] = tenant.ID
|
||||||
return tenant.ID, nil
|
return tenant.ID, nil
|
||||||
@@ -440,12 +481,19 @@ func (s *orgChartService) ensureOrgPath(ctx context.Context, rootTenantID string
|
|||||||
currentPath := ""
|
currentPath := ""
|
||||||
for i, part := range parts {
|
for i, part := range parts {
|
||||||
part = strings.TrimSpace(part)
|
part = strings.TrimSpace(part)
|
||||||
if part == "" || part == "-" { continue }
|
if part == "" || part == "-" {
|
||||||
if currentPath == "" { currentPath = part } else { currentPath += "/" + part }
|
continue
|
||||||
|
}
|
||||||
|
if currentPath == "" {
|
||||||
|
currentPath = part
|
||||||
|
} else {
|
||||||
|
currentPath += "/" + part
|
||||||
|
}
|
||||||
|
|
||||||
cacheKey := rootTenantID + ":" + currentPath
|
cacheKey := rootTenantID + ":" + currentPath
|
||||||
if id, ok := cache[cacheKey]; ok {
|
if id, ok := cache[cacheKey]; ok {
|
||||||
currentParentID = id; continue
|
currentParentID = id
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
var existingID string
|
var existingID string
|
||||||
@@ -454,7 +502,8 @@ func (s *orgChartService) ensureOrgPath(ctx context.Context, rootTenantID string
|
|||||||
isTopMatch := (g.ParentID == nil && currentParentID == rootTenantID)
|
isTopMatch := (g.ParentID == nil && currentParentID == rootTenantID)
|
||||||
isSubMatch := (g.ParentID != nil && *g.ParentID == currentParentID)
|
isSubMatch := (g.ParentID != nil && *g.ParentID == currentParentID)
|
||||||
if g.Name == part && (isTopMatch || isSubMatch) {
|
if g.Name == part && (isTopMatch || isSubMatch) {
|
||||||
existingID = g.ID; break
|
existingID = g.ID
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -501,7 +550,11 @@ func (s *orgChartService) ensureOrgPath(ctx context.Context, rootTenantID string
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *orgChartService) guessUnitType(index, total int) string {
|
func (s *orgChartService) guessUnitType(index, total int) string {
|
||||||
if total == 1 { return "Team" }
|
if total == 1 {
|
||||||
if index == 0 { return "Division" }
|
return "Team"
|
||||||
|
}
|
||||||
|
if index == 0 {
|
||||||
|
return "Division"
|
||||||
|
}
|
||||||
return "Team"
|
return "Team"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"baron-sso-backend/internal/domain"
|
||||||
|
"baron-sso-backend/internal/repository"
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"baron-sso-backend/internal/domain"
|
|
||||||
"baron-sso-backend/internal/repository"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
"github.com/xuri/excelize/v2"
|
"github.com/xuri/excelize/v2"
|
||||||
@@ -241,9 +241,11 @@ func TestImportOrgChart_MessyHeader(t *testing.T) {
|
|||||||
func (m *mockKratosService) ListIdentitySessions(ctx context.Context, identityID string) ([]KratosSession, error) {
|
func (m *mockKratosService) ListIdentitySessions(ctx context.Context, identityID string) ([]KratosSession, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockKratosService) GetSession(ctx context.Context, sessionID string) (*KratosSession, error) {
|
func (m *mockKratosService) GetSession(ctx context.Context, sessionID string) (*KratosSession, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockKratosService) DeleteSession(ctx context.Context, sessionID string) error {
|
func (m *mockKratosService) DeleteSession(ctx context.Context, sessionID string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,9 +104,15 @@ func (s *tenantService) ListJoinedTenants(ctx context.Context, userID string) ([
|
|||||||
adminIDs, _ := s.keto.ListObjects(ctx, "Tenant", "admins", "User:"+userID)
|
adminIDs, _ := s.keto.ListObjects(ctx, "Tenant", "admins", "User:"+userID)
|
||||||
|
|
||||||
idMap := make(map[string]bool)
|
idMap := make(map[string]bool)
|
||||||
for _, id := range memberIDs { idMap[id] = true }
|
for _, id := range memberIDs {
|
||||||
for _, id := range ownerIDs { idMap[id] = true }
|
idMap[id] = true
|
||||||
for _, id := range adminIDs { idMap[id] = true }
|
}
|
||||||
|
for _, id := range ownerIDs {
|
||||||
|
idMap[id] = true
|
||||||
|
}
|
||||||
|
for _, id := range adminIDs {
|
||||||
|
idMap[id] = true
|
||||||
|
}
|
||||||
|
|
||||||
allIDs := make([]string, 0, len(idMap))
|
allIDs := make([]string, 0, len(idMap))
|
||||||
for id := range idMap {
|
for id := range idMap {
|
||||||
|
|||||||
@@ -6,8 +6,7 @@ const configuredWorkers = process.env.PLAYWRIGHT_WORKERS
|
|||||||
const skipWebServer =
|
const skipWebServer =
|
||||||
process.env.PLAYWRIGHT_SKIP_WEBSERVER === "1" ||
|
process.env.PLAYWRIGHT_SKIP_WEBSERVER === "1" ||
|
||||||
process.env.PLAYWRIGHT_SKIP_WEBSERVER === "true";
|
process.env.PLAYWRIGHT_SKIP_WEBSERVER === "true";
|
||||||
const baseURL =
|
const baseURL = process.env.PLAYWRIGHT_BASE_URL || "http://localhost:5174";
|
||||||
process.env.PLAYWRIGHT_BASE_URL || "http://localhost:5174";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read environment variables from file.
|
* Read environment variables from file.
|
||||||
|
|||||||
@@ -34,7 +34,10 @@ export const router = createBrowserRouter(
|
|||||||
{ path: "clients/:id", element: <ClientDetailsPage /> },
|
{ path: "clients/:id", element: <ClientDetailsPage /> },
|
||||||
{ path: "clients/:id/consents", element: <ClientConsentsPage /> },
|
{ path: "clients/:id/consents", element: <ClientConsentsPage /> },
|
||||||
{ path: "clients/:id/settings", element: <ClientGeneralPage /> },
|
{ path: "clients/:id/settings", element: <ClientGeneralPage /> },
|
||||||
{ path: "clients/:id/relationships", element: <ClientRelationsPage /> },
|
{
|
||||||
|
path: "clients/:id/relationships",
|
||||||
|
element: <ClientRelationsPage />,
|
||||||
|
},
|
||||||
{ path: "audit-logs", element: <AuditLogsPage /> },
|
{ path: "audit-logs", element: <AuditLogsPage /> },
|
||||||
{ path: "profile", element: <ProfilePage /> },
|
{ path: "profile", element: <ProfilePage /> },
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -465,10 +465,18 @@ function ClientConsentsPage() {
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
{filteredRows.length === 0 && !isLoading && !error ? (
|
{filteredRows.length === 0 && !isLoading && !error ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={7} className="h-32 text-center text-muted-foreground">
|
<TableCell
|
||||||
|
colSpan={7}
|
||||||
|
className="h-32 text-center text-muted-foreground"
|
||||||
|
>
|
||||||
<div className="flex flex-col items-center gap-2">
|
<div className="flex flex-col items-center gap-2">
|
||||||
<Search className="h-8 w-8 opacity-20" />
|
<Search className="h-8 w-8 opacity-20" />
|
||||||
<p>{t("msg.dev.clients.consents.empty", "No consents found.")}</p>
|
<p>
|
||||||
|
{t(
|
||||||
|
"msg.dev.clients.consents.empty",
|
||||||
|
"No consents found.",
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|||||||
@@ -110,7 +110,8 @@ function ClientRelationsPage() {
|
|||||||
if (isSuperAdmin) return true;
|
if (isSuperAdmin) return true;
|
||||||
if (!relationData?.items || !myUserId) return false;
|
if (!relationData?.items || !myUserId) return false;
|
||||||
return relationData.items.some(
|
return relationData.items.some(
|
||||||
(item) => item.subject === `User:${myUserId}` && item.relation === "admins"
|
(item) =>
|
||||||
|
item.subject === `User:${myUserId}` && item.relation === "admins",
|
||||||
);
|
);
|
||||||
}, [relationData?.items, myUserId, isSuperAdmin]);
|
}, [relationData?.items, myUserId, isSuperAdmin]);
|
||||||
|
|
||||||
@@ -664,7 +665,9 @@ function ClientRelationsPage() {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="gap-2 text-destructive hover:text-destructive"
|
className="gap-2 text-destructive hover:text-destructive"
|
||||||
disabled={removeMutation.isPending || !canManageRelations}
|
disabled={
|
||||||
|
removeMutation.isPending || !canManageRelations
|
||||||
|
}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
handleRemove(item.relation, item.subject)
|
handleRemove(item.relation, item.subject)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,14 @@ test.describe("DevFront relationships", () => {
|
|||||||
],
|
],
|
||||||
relations: {
|
relations: {
|
||||||
"client-rel": [
|
"client-rel": [
|
||||||
|
{
|
||||||
|
relation: "admins",
|
||||||
|
subject: "User:playwright-user",
|
||||||
|
subjectType: "User",
|
||||||
|
subjectId: "playwright-user",
|
||||||
|
userName: "Playwright User",
|
||||||
|
userEmail: "playwright@example.com",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
relation: "config_editor",
|
relation: "config_editor",
|
||||||
subject: "User:user-1",
|
subject: "User:user-1",
|
||||||
@@ -67,13 +75,16 @@ test.describe("DevFront relationships", () => {
|
|||||||
await page.getByLabel(/동의 조회/).check();
|
await page.getByLabel(/동의 조회/).check();
|
||||||
await page.getByRole("button", { name: /^추가$/ }).click();
|
await page.getByRole("button", { name: /^추가$/ }).click();
|
||||||
|
|
||||||
await expect(page.getByText("User:user-2")).toBeVisible();
|
await expect(
|
||||||
await expect.poll(() => state.relations["client-rel"]?.length ?? 0).toBe(3);
|
page.locator("tr").filter({ hasText: "User:user-2" }).first(),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect.poll(() => state.relations["client-rel"]?.length ?? 0).toBe(4);
|
||||||
|
|
||||||
await page
|
await page
|
||||||
.locator("tr")
|
.locator("tr")
|
||||||
.filter({ hasText: "User:user-2" })
|
.filter({ hasText: "User:user-2" })
|
||||||
.getByRole("button", { name: /Delete|삭제/i })
|
.getByRole("button", { name: /Delete|삭제/i })
|
||||||
|
.first()
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
await expect
|
await expect
|
||||||
|
|||||||
@@ -32,7 +32,9 @@ test.describe("DevFront role report", () => {
|
|||||||
page.getByText(/조회 가능한 RP가 없습니다|No RPs are available/i),
|
page.getByText(/조회 가능한 RP가 없습니다|No RPs are available/i),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
await expect(
|
await expect(
|
||||||
page.getByText(/연동 앱|Connected Application/i),
|
page.getByRole("heading", {
|
||||||
|
name: /^연동 앱$|^Connected Application$/i,
|
||||||
|
}),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
await captureEvidence(page, testInfo, "role-user-empty-rps");
|
await captureEvidence(page, testInfo, "role-user-empty-rps");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -497,6 +497,9 @@ rp_admin = "RP administrators can only access resources for the apps they manage
|
|||||||
tenant_admin = "Tenant administrator permissions are not configured correctly or have expired."
|
tenant_admin = "Tenant administrator permissions are not configured correctly or have expired."
|
||||||
title = "Access Denied: {{resource}}"
|
title = "Access Denied: {{resource}}"
|
||||||
user = "Regular users cannot access the developer console."
|
user = "Regular users cannot access the developer console."
|
||||||
|
user.audit = "Viewing audit logs for this App (RP) is only available when granted 'RP Admin' or 'Audit View' relationships. If you need access, please request it from an administrator."
|
||||||
|
user.clients = "General user accounts can only use this feature if they have been granted operational or management relationships for the relevant RP (App). If you need access, please request it from an administrator."
|
||||||
|
user.consents = "Viewing consent history for this App (RP) is only available when granted 'RP Admin', 'Consent View', or 'Consent Revoke' relationships. If you need access, please request it from an administrator."
|
||||||
|
|
||||||
[msg.dev.sidebar]
|
[msg.dev.sidebar]
|
||||||
notice = "Developer Console"
|
notice = "Developer Console"
|
||||||
|
|||||||
@@ -899,6 +899,9 @@ rp_admin = "RP 관리자는 담당 앱의 리소스만 조회할 수 있습니
|
|||||||
tenant_admin = "테넌트 관리자 권한이 올바르게 설정되지 않았거나 만료되었습니다."
|
tenant_admin = "테넌트 관리자 권한이 올바르게 설정되지 않았거나 만료되었습니다."
|
||||||
title = "{{resource}} 접근 권한 없음"
|
title = "{{resource}} 접근 권한 없음"
|
||||||
user = "일반 사용자는 관리자 화면에 접근할 수 없습니다."
|
user = "일반 사용자는 관리자 화면에 접근할 수 없습니다."
|
||||||
|
user.audit = "해당 앱(RP)에 대한 감사 로그 조회는 'RP 관리자', '감사 조회' 관계가 부여된 경우에만 사용할 수 있습니다. 권한이 필요하면 관리자에게 요청하세요."
|
||||||
|
user.clients = "일반 사용자 계정은 담당 RP(앱)에 대한 운영 또는 관리 관계가 부여된 경우에만 해당 기능을 사용할 수 있습니다. 권한이 필요하면 관리자에게 요청하세요."
|
||||||
|
user.consents = "해당 앱(RP)에 대한 동의 내역 조회는 'RP 관리자', '동의 조회', '동의 회수' 관계가 부여된 경우에만 사용할 수 있습니다. 권한이 필요하면 관리자에게 요청하세요."
|
||||||
|
|
||||||
[msg.dev.sidebar]
|
[msg.dev.sidebar]
|
||||||
notice = "개발자 전용 콘솔입니다."
|
notice = "개발자 전용 콘솔입니다."
|
||||||
|
|||||||
@@ -774,6 +774,9 @@ rp_admin = ""
|
|||||||
tenant_admin = ""
|
tenant_admin = ""
|
||||||
title = ""
|
title = ""
|
||||||
user = ""
|
user = ""
|
||||||
|
user.audit = ""
|
||||||
|
user.clients = ""
|
||||||
|
user.consents = ""
|
||||||
|
|
||||||
[msg.dev.sidebar]
|
[msg.dev.sidebar]
|
||||||
notice = ""
|
notice = ""
|
||||||
|
|||||||
@@ -70,9 +70,11 @@ if [ "$provision_exit_code" -ne 0 ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
set +e
|
set +e
|
||||||
|
port="$(node -e "const net=require('node:net'); const s=net.createServer(); s.listen(0,'127.0.0.1',()=>{console.log(s.address().port); s.close();});")"
|
||||||
|
echo "==> adminfront using PORT=$port"
|
||||||
(
|
(
|
||||||
cd adminfront
|
cd adminfront
|
||||||
npm test
|
PORT="$port" PLAYWRIGHT_WORKERS="${PLAYWRIGHT_WORKERS:-1}" npm test
|
||||||
) 2>&1 | tee reports/adminfront-test.log
|
) 2>&1 | tee reports/adminfront-test.log
|
||||||
test_exit_code=${PIPESTATUS[0]}
|
test_exit_code=${PIPESTATUS[0]}
|
||||||
set -e
|
set -e
|
||||||
|
|||||||
Reference in New Issue
Block a user