forked from baron/baron-sso
테넌트 CSV 조직 설정 동기화 보완
This commit is contained in:
18
adminfront/hanmac_org.csv
Normal file
18
adminfront/hanmac_org.csv
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
"조직명","멤버 수","조직장","조직 다국어명","설명","메일링 리스트","마스터에게 메시지방 기능 권한 부여","조직 관련 알림 보내기","조직 공개","외부 도메인 메일 수신 차단","보내는 주소로 사용 가능한 구성원","메일을 보낼 수 있는 구성원","상위 조직"
|
||||||
|
"네이버웍스관리용","2","슈퍼관리자(su-@samaneng.com)","","","su2@hanmaceng.co.kr","N","N","N","Y","","",""
|
||||||
|
"영업지원","0","","","","t_686ee@hanmaceng.co.kr","Y","N","Y","Y","","",""
|
||||||
|
"안전관리부","0","","","","t_993iv@hanmaceng.co.kr","Y","N","Y","Y","","",""
|
||||||
|
"인프라사업본부","0","","","","t_211qf@hanmaceng.co.kr","Y","N","Y","Y","","",""
|
||||||
|
"도로부","0","","","","t_471lj@hanmaceng.co.kr","Y","N","Y","Y","","","인프라사업본부(t_211qf@hanmaceng.co.kr)"
|
||||||
|
"교통부","0","","","","t_571hb@hanmaceng.co.kr","Y","N","Y","Y","","","인프라사업본부(t_211qf@hanmaceng.co.kr)"
|
||||||
|
"구조부","0","","","","t_758pk@hanmaceng.co.kr","Y","N","Y","Y","","","인프라사업본부(t_211qf@hanmaceng.co.kr)"
|
||||||
|
"지반터널부","0","","","","t_113ok@hanmaceng.co.kr","Y","N","Y","Y","","","인프라사업본부(t_211qf@hanmaceng.co.kr)"
|
||||||
|
"국토환경사업본부","0","","","","t_892ed@hanmaceng.co.kr","Y","N","Y","Y","","",""
|
||||||
|
"환경평가부","0","","","","t_021ay@hanmaceng.co.kr","Y","N","Y","Y","","","국토환경사업본부(t_892ed@hanmaceng.co.kr)"
|
||||||
|
"도시계획부","0","","","","t_501xv@hanmaceng.co.kr","Y","N","Y","Y","","","국토환경사업본부(t_892ed@hanmaceng.co.kr)"
|
||||||
|
"수자원부","0","","","","t_420zr@hanmaceng.co.kr","Y","N","Y","Y","","","국토환경사업본부(t_892ed@hanmaceng.co.kr)"
|
||||||
|
"상하수도부","0","","","","t_979ca@hanmaceng.co.kr","Y","N","Y","Y","","","국토환경사업본부(t_892ed@hanmaceng.co.kr)"
|
||||||
|
"건설사업관리본부","0","","","","t_504fh@hanmaceng.co.kr","Y","N","Y","Y","","",""
|
||||||
|
"건설사업부","0","","","","t_756my@hanmaceng.co.kr","Y","N","Y","Y","","","건설사업관리본부(t_504fh@hanmaceng.co.kr)"
|
||||||
|
"안전진단부","0","","","","t_470ta@hanmaceng.co.kr","Y","N","Y","Y","","",""
|
||||||
|
"경영지원부","0","","","","t_804xh@hanmaceng.co.kr","Y","N","Y","Y","","",""
|
||||||
|
18
adminfront/hanmac_org_slugged.csv
Normal file
18
adminfront/hanmac_org_slugged.csv
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
"조직명","멤버 수","조직장","조직 다국어명","설명","메일링 리스트","마스터에게 메시지방 기능 권한 부여","조직 관련 알림 보내기","조직 공개","외부 도메인 메일 수신 차단","보내는 주소로 사용 가능한 구성원","메일을 보낼 수 있는 구성원","상위 조직"
|
||||||
|
"네이버웍스관리용","2","슈퍼관리자(su-@samaneng.com)","","","nw-admin-hanmac@hanmaceng.co.kr","N","N","N","Y","","",""
|
||||||
|
"영업지원","0","","","","sales-support@hanmaceng.co.kr","Y","N","Y","Y","","",""
|
||||||
|
"안전관리부","0","","","","safety-management@hanmaceng.co.kr","Y","N","Y","Y","","",""
|
||||||
|
"인프라사업본부","0","","","","infrastructure-hq@hanmaceng.co.kr","Y","N","Y","Y","","",""
|
||||||
|
"도로부","0","","","","infra-road@hanmaceng.co.kr","Y","N","Y","Y","","","인프라사업본부(infrastructure-hq@hanmaceng.co.kr)"
|
||||||
|
"교통부","0","","","","traffic@hanmaceng.co.kr","Y","N","Y","Y","","","인프라사업본부(infrastructure-hq@hanmaceng.co.kr)"
|
||||||
|
"구조부","0","","","","infra-structures@hanmaceng.co.kr","Y","N","Y","Y","","","인프라사업본부(infrastructure-hq@hanmaceng.co.kr)"
|
||||||
|
"지반터널부","0","","","","infra-geotech-tunnel@hanmaceng.co.kr","Y","N","Y","Y","","","인프라사업본부(infrastructure-hq@hanmaceng.co.kr)"
|
||||||
|
"국토환경사업본부","0","","","","land-environment-hq@hanmaceng.co.kr","Y","N","Y","Y","","",""
|
||||||
|
"환경평가부","0","","","","land-env-assessment@hanmaceng.co.kr","Y","N","Y","Y","","","국토환경사업본부(land-environment-hq@hanmaceng.co.kr)"
|
||||||
|
"도시계획부","0","","","","land-env-urban-planning@hanmaceng.co.kr","Y","N","Y","Y","","","국토환경사업본부(land-environment-hq@hanmaceng.co.kr)"
|
||||||
|
"수자원부","0","","","","land-env-water-resources@hanmaceng.co.kr","Y","N","Y","Y","","","국토환경사업본부(land-environment-hq@hanmaceng.co.kr)"
|
||||||
|
"상하수도부","0","","","","water-sewerage@hanmaceng.co.kr","Y","N","Y","Y","","","국토환경사업본부(land-environment-hq@hanmaceng.co.kr)"
|
||||||
|
"건설사업관리본부","0","","","","construction-management-hq@hanmaceng.co.kr","Y","N","Y","Y","","",""
|
||||||
|
"건설사업부","0","","","","construction-business@hanmaceng.co.kr","Y","N","Y","Y","","","건설사업관리본부(construction-management-hq@hanmaceng.co.kr)"
|
||||||
|
"안전진단부","0","","","","safety-diagnosis@hanmaceng.co.kr","Y","N","Y","Y","","",""
|
||||||
|
"경영지원부","0","","","","management-support@hanmaceng.co.kr","Y","N","Y","Y","","",""
|
||||||
|
@@ -80,7 +80,7 @@ import {
|
|||||||
} from "../utils/tenantCsvImport";
|
} from "../utils/tenantCsvImport";
|
||||||
|
|
||||||
const tenantCSVTemplate =
|
const tenantCSVTemplate =
|
||||||
"name,type,parent_tenant_slug,slug,memo,email_domain\n";
|
"name,type,parent_tenant_slug,slug,memo,email_domain,visibility,org_unit_type\n";
|
||||||
|
|
||||||
type SortConfig = {
|
type SortConfig = {
|
||||||
key: keyof TenantSummary | "recursiveMemberCount";
|
key: keyof TenantSummary | "recursiveMemberCount";
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ describe("tenantCsvImport", () => {
|
|||||||
|
|
||||||
it("parses tenant CSV rows with the supported import columns", () => {
|
it("parses tenant CSV rows with the supported import columns", () => {
|
||||||
const rows = parseTenantCSV(
|
const rows = parseTenantCSV(
|
||||||
"tenant_id,name,type,parent_tenant_id,slug,memo,email_domain\n,Hanmac Tech,COMPANY,,hanmac-tech,Memo,hanmac-tech.example.com\n",
|
"tenant_id,name,type,parent_tenant_id,slug,memo,email_domain,visibility,org_unit_type\n,Hanmac Tech,COMPANY,,hanmac-tech,Memo,hanmac-tech.example.com,internal,센터\n",
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(rows).toEqual([
|
expect(rows).toEqual([
|
||||||
@@ -80,6 +80,8 @@ describe("tenantCsvImport", () => {
|
|||||||
slug: "hanmac-tech",
|
slug: "hanmac-tech",
|
||||||
memo: "Memo",
|
memo: "Memo",
|
||||||
emailDomain: "hanmac-tech.example.com",
|
emailDomain: "hanmac-tech.example.com",
|
||||||
|
visibility: "internal",
|
||||||
|
orgUnitType: "센터",
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
@@ -109,15 +111,18 @@ describe("tenantCsvImport", () => {
|
|||||||
|
|
||||||
it("serializes selected matches by filling tenant_id before upload", () => {
|
it("serializes selected matches by filling tenant_id before upload", () => {
|
||||||
const rows = parseTenantCSV(
|
const rows = parseTenantCSV(
|
||||||
"tenant_id,name,type,parent_tenant_id,slug,memo,email_domain\n,Hanmac Tech,COMPANY,,hanmac-tech,Memo,hanmac-tech.example.com\n",
|
"tenant_id,name,type,parent_tenant_id,slug,memo,email_domain,visibility,org_unit_type\n,Hanmac Tech,COMPANY,,hanmac-tech,Memo,hanmac-tech.example.com,private,팀\n",
|
||||||
);
|
);
|
||||||
const preview = buildTenantImportPreview(rows, tenants);
|
const preview = buildTenantImportPreview(rows, tenants);
|
||||||
const csv = serializeTenantImportCSV(preview, {
|
const csv = serializeTenantImportCSV(preview, {
|
||||||
2: "tenant-1",
|
2: "tenant-1",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
expect(csv.split("\n")[0]).toBe(
|
||||||
|
"tenant_id,name,type,parent_tenant_id,parent_tenant_slug,slug,memo,email_domain,visibility,org_unit_type",
|
||||||
|
);
|
||||||
expect(csv).toContain(
|
expect(csv).toContain(
|
||||||
"tenant-1,Hanmac Tech,COMPANY,,,hanmac-tech,Memo,hanmac-tech.example.com",
|
"tenant-1,Hanmac Tech,COMPANY,,,hanmac-tech,Memo,hanmac-tech.example.com,private,팀",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -228,7 +233,7 @@ describe("tenantCsvImport", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(csv.split("\n")[0]).toBe(
|
expect(csv.split("\n")[0]).toBe(
|
||||||
"tenant_id,name,type,parent_tenant_id,parent_tenant_slug,slug,memo,email_domain",
|
"tenant_id,name,type,parent_tenant_id,parent_tenant_slug,slug,memo,email_domain,visibility,org_unit_type",
|
||||||
);
|
);
|
||||||
expect(csv).toContain(
|
expect(csv).toContain(
|
||||||
"staging-child-id,Child Tenant,ORGANIZATION,staging-parent-id,parent-slug,child-slug,,",
|
"staging-child-id,Child Tenant,ORGANIZATION,staging-parent-id,parent-slug,child-slug,,",
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ export type TenantCSVRow = {
|
|||||||
slug: string;
|
slug: string;
|
||||||
memo: string;
|
memo: string;
|
||||||
emailDomain: string;
|
emailDomain: string;
|
||||||
|
visibility: string;
|
||||||
|
orgUnitType: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TenantCSVParseOptions = {
|
export type TenantCSVParseOptions = {
|
||||||
@@ -76,6 +78,8 @@ const importHeaders = [
|
|||||||
"slug",
|
"slug",
|
||||||
"memo",
|
"memo",
|
||||||
"email_domain",
|
"email_domain",
|
||||||
|
"visibility",
|
||||||
|
"org_unit_type",
|
||||||
];
|
];
|
||||||
|
|
||||||
const headerAliases: Record<string, TenantCSVSourceKey> = {
|
const headerAliases: Record<string, TenantCSVSourceKey> = {
|
||||||
@@ -102,6 +106,16 @@ const headerAliases: Record<string, TenantCSVSourceKey> = {
|
|||||||
email_domain: "emailDomain",
|
email_domain: "emailDomain",
|
||||||
domain: "emailDomain",
|
domain: "emailDomain",
|
||||||
domains: "emailDomain",
|
domains: "emailDomain",
|
||||||
|
visibility: "visibility",
|
||||||
|
public_setting: "visibility",
|
||||||
|
publicsetting: "visibility",
|
||||||
|
orgunittype: "orgUnitType",
|
||||||
|
org_unit_type: "orgUnitType",
|
||||||
|
"org-unit-type": "orgUnitType",
|
||||||
|
organizationtype: "orgUnitType",
|
||||||
|
organization_type: "orgUnitType",
|
||||||
|
orgtype: "orgUnitType",
|
||||||
|
org_type: "orgUnitType",
|
||||||
};
|
};
|
||||||
|
|
||||||
export function parseTenantCSV(
|
export function parseTenantCSV(
|
||||||
@@ -159,6 +173,8 @@ export function parseTenantCSV(
|
|||||||
slug,
|
slug,
|
||||||
memo: value("memo"),
|
memo: value("memo"),
|
||||||
emailDomain: value("emailDomain"),
|
emailDomain: value("emailDomain"),
|
||||||
|
visibility: value("visibility"),
|
||||||
|
orgUnitType: value("orgUnitType"),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -287,6 +303,8 @@ export function serializeTenantImportCSV(
|
|||||||
slug,
|
slug,
|
||||||
preview.row.memo,
|
preview.row.memo,
|
||||||
preview.row.emailDomain,
|
preview.row.emailDomain,
|
||||||
|
preview.row.visibility,
|
||||||
|
preview.row.orgUnitType,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
return `${lines.map(formatCSVRecord).join("\n")}\n`;
|
return `${lines.map(formatCSVRecord).join("\n")}\n`;
|
||||||
|
|||||||
@@ -110,6 +110,8 @@ type tenantCSVRecord struct {
|
|||||||
Slug string
|
Slug string
|
||||||
Memo string
|
Memo string
|
||||||
Domains []string
|
Domains []string
|
||||||
|
Visibility string
|
||||||
|
OrgUnitType string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *TenantHandler) RegisterTenantPublic(c *fiber.Ctx) error {
|
func (h *TenantHandler) RegisterTenantPublic(c *fiber.Ctx) error {
|
||||||
@@ -278,10 +280,10 @@ func (h *TenantHandler) ExportTenantsCSV(c *fiber.Ctx) error {
|
|||||||
writer := csv.NewWriter(&buf)
|
writer := csv.NewWriter(&buf)
|
||||||
includeIDs := includeCSVIds(c)
|
includeIDs := includeCSVIds(c)
|
||||||
if includeIDs {
|
if includeIDs {
|
||||||
if err := writer.Write([]string{"tenant_id", "name", "type", "parent_tenant_id", "parent_tenant_slug", "slug", "memo", "email_domain"}); err != nil {
|
if err := writer.Write([]string{"tenant_id", "name", "type", "parent_tenant_id", "parent_tenant_slug", "slug", "memo", "email_domain", "visibility", "org_unit_type"}); err != nil {
|
||||||
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
|
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
|
||||||
}
|
}
|
||||||
} else if err := writer.Write([]string{"name", "type", "parent_tenant_slug", "slug", "memo", "email_domain"}); err != nil {
|
} else if err := writer.Write([]string{"name", "type", "parent_tenant_slug", "slug", "memo", "email_domain", "visibility", "org_unit_type"}); err != nil {
|
||||||
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
|
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
|
||||||
}
|
}
|
||||||
slugByID := make(map[string]string, len(tenants))
|
slugByID := make(map[string]string, len(tenants))
|
||||||
@@ -302,6 +304,7 @@ func (h *TenantHandler) ExportTenantsCSV(c *fiber.Ctx) error {
|
|||||||
domains = append(domains, domainName)
|
domains = append(domains, domainName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
visibility, orgUnitType := tenantCSVOrgConfigValues(tenant.Config)
|
||||||
row := []string{
|
row := []string{
|
||||||
tenant.Name,
|
tenant.Name,
|
||||||
tenant.Type,
|
tenant.Type,
|
||||||
@@ -309,6 +312,8 @@ func (h *TenantHandler) ExportTenantsCSV(c *fiber.Ctx) error {
|
|||||||
tenant.Slug,
|
tenant.Slug,
|
||||||
tenant.Description,
|
tenant.Description,
|
||||||
strings.Join(domains, ";"),
|
strings.Join(domains, ";"),
|
||||||
|
visibility,
|
||||||
|
orgUnitType,
|
||||||
}
|
}
|
||||||
if includeIDs {
|
if includeIDs {
|
||||||
row = []string{
|
row = []string{
|
||||||
@@ -320,6 +325,8 @@ func (h *TenantHandler) ExportTenantsCSV(c *fiber.Ctx) error {
|
|||||||
tenant.Slug,
|
tenant.Slug,
|
||||||
tenant.Description,
|
tenant.Description,
|
||||||
strings.Join(domains, ";"),
|
strings.Join(domains, ";"),
|
||||||
|
visibility,
|
||||||
|
orgUnitType,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := writer.Write(row); err != nil {
|
if err := writer.Write(row); err != nil {
|
||||||
@@ -501,6 +508,8 @@ func parseTenantCSVRecords(r io.Reader) ([]tenantCSVRecord, error) {
|
|||||||
Slug: slug,
|
Slug: slug,
|
||||||
Memo: tenantCSVValue(row, header, "memo"),
|
Memo: tenantCSVValue(row, header, "memo"),
|
||||||
Domains: splitTenantCSVDomains(tenantCSVValue(row, header, "email_domain")),
|
Domains: splitTenantCSVDomains(tenantCSVValue(row, header, "email_domain")),
|
||||||
|
Visibility: tenantCSVValue(row, header, "visibility"),
|
||||||
|
OrgUnitType: tenantCSVValue(row, header, "org_unit_type"),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -529,6 +538,16 @@ func tenantCSVHeaderIndex(header []string) map[string]int {
|
|||||||
"email_domain": "email_domain",
|
"email_domain": "email_domain",
|
||||||
"domain": "email_domain",
|
"domain": "email_domain",
|
||||||
"domains": "email_domain",
|
"domains": "email_domain",
|
||||||
|
"visibility": "visibility",
|
||||||
|
"public_setting": "visibility",
|
||||||
|
"publicsetting": "visibility",
|
||||||
|
"orgunittype": "org_unit_type",
|
||||||
|
"org_unit_type": "org_unit_type",
|
||||||
|
"org-unit-type": "org_unit_type",
|
||||||
|
"organizationtype": "org_unit_type",
|
||||||
|
"organization_type": "org_unit_type",
|
||||||
|
"orgtype": "org_unit_type",
|
||||||
|
"org_type": "org_unit_type",
|
||||||
}
|
}
|
||||||
for i, column := range header {
|
for i, column := range header {
|
||||||
key := strings.ToLower(strings.TrimSpace(column))
|
key := strings.ToLower(strings.TrimSpace(column))
|
||||||
@@ -745,6 +764,45 @@ func tenantVisibility(config domain.JSONMap) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func tenantCSVOrgConfigValues(config domain.JSONMap) (string, string) {
|
||||||
|
visibility := tenantVisibility(config)
|
||||||
|
orgUnitType, _ := config["orgUnitType"].(string)
|
||||||
|
return visibility, strings.TrimSpace(orgUnitType)
|
||||||
|
}
|
||||||
|
|
||||||
|
func tenantCSVRecordConfig(record tenantCSVRecord) (domain.JSONMap, error) {
|
||||||
|
config := map[string]any{}
|
||||||
|
if strings.TrimSpace(record.Visibility) != "" {
|
||||||
|
config["visibility"] = record.Visibility
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(record.OrgUnitType) != "" {
|
||||||
|
config["orgUnitType"] = record.OrgUnitType
|
||||||
|
}
|
||||||
|
if len(config) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return normalizeTenantConfig(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergeTenantCSVRecordConfig(current domain.JSONMap, record tenantCSVRecord) (domain.JSONMap, bool, error) {
|
||||||
|
recordConfig, err := tenantCSVRecordConfig(record)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
if len(recordConfig) == 0 {
|
||||||
|
return current, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
merged := make(domain.JSONMap, len(current)+len(recordConfig))
|
||||||
|
for key, value := range current {
|
||||||
|
merged[key] = value
|
||||||
|
}
|
||||||
|
for key, value := range recordConfig {
|
||||||
|
merged[key] = value
|
||||||
|
}
|
||||||
|
return merged, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
func filterPublicTenants(tenants []domain.Tenant) []domain.Tenant {
|
func filterPublicTenants(tenants []domain.Tenant) []domain.Tenant {
|
||||||
excludedIDs := make(map[string]bool)
|
excludedIDs := make(map[string]bool)
|
||||||
for _, tenant := range tenants {
|
for _, tenant := range tenants {
|
||||||
@@ -963,6 +1021,13 @@ func (h *TenantHandler) upsertTenantCSVRecord(c *fiber.Ctx, record tenantCSVReco
|
|||||||
if tenant.Status == "" {
|
if tenant.Status == "" {
|
||||||
tenant.Status = domain.TenantStatusActive
|
tenant.Status = domain.TenantStatusActive
|
||||||
}
|
}
|
||||||
|
mergedConfig, changedConfig, err := mergeTenantCSVRecordConfig(tenant.Config, record)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
if changedConfig {
|
||||||
|
tenant.Config = mergedConfig
|
||||||
|
}
|
||||||
|
|
||||||
if err := h.DB.Save(&tenant).Error; err != nil {
|
if err := h.DB.Save(&tenant).Error; err != nil {
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
@@ -999,6 +1064,13 @@ func (h *TenantHandler) createTenantCSVRecord(c *fiber.Ctx, record tenantCSVReco
|
|||||||
Description: record.Memo,
|
Description: record.Memo,
|
||||||
Status: domain.TenantStatusActive,
|
Status: domain.TenantStatusActive,
|
||||||
}
|
}
|
||||||
|
config, _, err := mergeTenantCSVRecordConfig(nil, record)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(config) > 0 {
|
||||||
|
tenant.Config = config
|
||||||
|
}
|
||||||
if err := h.DB.Create(&tenant).Error; err != nil {
|
if err := h.DB.Create(&tenant).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -1041,6 +1113,22 @@ func (h *TenantHandler) createTenantCSVRecord(c *fiber.Ctx, record tenantCSVReco
|
|||||||
}
|
}
|
||||||
|
|
||||||
tenant, err := h.Service.RegisterTenant(c.Context(), record.Name, record.Slug, record.Type, record.Memo, record.Domains, record.ParentTenantID, creatorID)
|
tenant, err := h.Service.RegisterTenant(c.Context(), record.Name, record.Slug, record.Type, record.Memo, record.Domains, record.ParentTenantID, creatorID)
|
||||||
|
if err != nil || tenant == nil {
|
||||||
|
return tenant, err
|
||||||
|
}
|
||||||
|
config, changedConfig, err := mergeTenantCSVRecordConfig(tenant.Config, record)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if changedConfig {
|
||||||
|
if h.DB == nil {
|
||||||
|
return nil, errors.New("database not available for tenant config import")
|
||||||
|
}
|
||||||
|
tenant.Config = config
|
||||||
|
if err := h.DB.Save(tenant).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
return tenant, err
|
return tenant, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -454,6 +454,10 @@ func TestTenantHandler_ExportTenantsCSV(t *testing.T) {
|
|||||||
ParentID: &parentID,
|
ParentID: &parentID,
|
||||||
Slug: "tenant-a",
|
Slug: "tenant-a",
|
||||||
Description: "Primary tenant",
|
Description: "Primary tenant",
|
||||||
|
Config: domain.JSONMap{
|
||||||
|
"visibility": "internal",
|
||||||
|
"orgUnitType": "센터",
|
||||||
|
},
|
||||||
Domains: []domain.TenantDomain{
|
Domains: []domain.TenantDomain{
|
||||||
{Domain: "tenant-a.example.com"},
|
{Domain: "tenant-a.example.com"},
|
||||||
{Domain: "login.tenant-a.example.com"},
|
{Domain: "login.tenant-a.example.com"},
|
||||||
@@ -470,8 +474,8 @@ func TestTenantHandler_ExportTenantsCSV(t *testing.T) {
|
|||||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
assert.Contains(t, resp.Header.Get("Content-Disposition"), "tenants.csv")
|
assert.Contains(t, resp.Header.Get("Content-Disposition"), "tenants.csv")
|
||||||
assert.Equal(t, "text/csv", strings.Split(resp.Header.Get("Content-Type"), ";")[0])
|
assert.Equal(t, "text/csv", strings.Split(resp.Header.Get("Content-Type"), ";")[0])
|
||||||
assert.Contains(t, string(body), "tenant_id,name,type,parent_tenant_id,parent_tenant_slug,slug,memo,email_domain")
|
assert.Contains(t, string(body), "tenant_id,name,type,parent_tenant_id,parent_tenant_slug,slug,memo,email_domain,visibility,org_unit_type")
|
||||||
assert.Contains(t, string(body), "t1,Tenant A,COMPANY,parent-1,,tenant-a,Primary tenant,tenant-a.example.com;login.tenant-a.example.com")
|
assert.Contains(t, string(body), "t1,Tenant A,COMPANY,parent-1,,tenant-a,Primary tenant,tenant-a.example.com;login.tenant-a.example.com,internal,센터")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTenantHandler_ExportTenantsCSV_OmitsIDsAndUsesParentSlug(t *testing.T) {
|
func TestTenantHandler_ExportTenantsCSV_OmitsIDsAndUsesParentSlug(t *testing.T) {
|
||||||
@@ -506,7 +510,7 @@ func TestTenantHandler_ExportTenantsCSV_OmitsIDsAndUsesParentSlug(t *testing.T)
|
|||||||
text := string(body)
|
text := string(body)
|
||||||
|
|
||||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
assert.Contains(t, text, "name,type,parent_tenant_slug,slug,memo,email_domain")
|
assert.Contains(t, text, "name,type,parent_tenant_slug,slug,memo,email_domain,visibility,org_unit_type")
|
||||||
assert.Contains(t, text, "Child Tenant,USER_GROUP,parent-tenant,child-tenant,,")
|
assert.Contains(t, text, "Child Tenant,USER_GROUP,parent-tenant,child-tenant,,")
|
||||||
assert.NotContains(t, text, "tenant_id")
|
assert.NotContains(t, text, "tenant_id")
|
||||||
assert.NotContains(t, text, "parent_tenant_id")
|
assert.NotContains(t, text, "parent_tenant_id")
|
||||||
@@ -663,13 +667,15 @@ func TestNormalizeTenantTypeAllowsOrganization(t *testing.T) {
|
|||||||
|
|
||||||
func TestTenantCSVAllowedDomainsRoundTrip(t *testing.T) {
|
func TestTenantCSVAllowedDomainsRoundTrip(t *testing.T) {
|
||||||
records, err := parseTenantCSVRecords(strings.NewReader(
|
records, err := parseTenantCSVRecords(strings.NewReader(
|
||||||
"name,type,parent_tenant_slug,slug,memo,email_domain\n" +
|
"name,type,parent_tenant_slug,slug,memo,email_domain,visibility,org_unit_type\n" +
|
||||||
"Hanmac,COMPANY,,hanmac,,\"samaneng.com, hanmaceng.co.kr;login.hmac.kr\"\n",
|
"Hanmac,COMPANY,,hanmac,,\"samaneng.com, hanmaceng.co.kr;login.hmac.kr\",internal,센터\n",
|
||||||
))
|
))
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, records, 1)
|
assert.Len(t, records, 1)
|
||||||
assert.Equal(t, []string{"samaneng.com", "hanmaceng.co.kr", "login.hmac.kr"}, records[0].Domains)
|
assert.Equal(t, []string{"samaneng.com", "hanmaceng.co.kr", "login.hmac.kr"}, records[0].Domains)
|
||||||
|
assert.Equal(t, "internal", records[0].Visibility)
|
||||||
|
assert.Equal(t, "센터", records[0].OrgUnitType)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNormalizeTenantDomainInputsSplitsCommaAndWhitespace(t *testing.T) {
|
func TestNormalizeTenantDomainInputsSplitsCommaAndWhitespace(t *testing.T) {
|
||||||
|
|||||||
73
orgfront/src/features/auth/LoginPage.test.tsx
Normal file
73
orgfront/src/features/auth/LoginPage.test.tsx
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { act } from "react";
|
||||||
|
import { type Root, createRoot } from "react-dom/client";
|
||||||
|
import { MemoryRouter } from "react-router-dom";
|
||||||
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import LoginPage from "./LoginPage";
|
||||||
|
|
||||||
|
const authState = {
|
||||||
|
isAuthenticated: false,
|
||||||
|
isLoading: false,
|
||||||
|
activeNavigator: undefined as string | undefined,
|
||||||
|
signinRedirect: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.mock("react-oidc-context", () => ({
|
||||||
|
useAuth: () => authState,
|
||||||
|
}));
|
||||||
|
|
||||||
|
function renderLoginPage(initialEntry: string) {
|
||||||
|
const container = document.createElement("div");
|
||||||
|
document.body.appendChild(container);
|
||||||
|
const root = createRoot(container);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
root.render(
|
||||||
|
<MemoryRouter initialEntries={[initialEntry]}>
|
||||||
|
<LoginPage />
|
||||||
|
</MemoryRouter>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return { container, root };
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanupRendered(container: HTMLDivElement, root: Root) {
|
||||||
|
act(() => {
|
||||||
|
root.unmount();
|
||||||
|
});
|
||||||
|
container.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("OrgFront LoginPage auto login", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
authState.isAuthenticated = false;
|
||||||
|
authState.isLoading = false;
|
||||||
|
authState.activeNavigator = undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not start auto login again when the orgfront session is already authenticated", () => {
|
||||||
|
authState.isAuthenticated = true;
|
||||||
|
|
||||||
|
const rendered = renderLoginPage(
|
||||||
|
"/login?auto=1&returnTo=%2Fembed%2Fpicker%3Fmode%3Dsingle",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(authState.signinRedirect).not.toHaveBeenCalled();
|
||||||
|
cleanupRendered(rendered.container, rendered.root);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("starts auto login once when auto mode is requested without an authenticated session", () => {
|
||||||
|
const rendered = renderLoginPage(
|
||||||
|
"/login?auto=1&returnTo=%2Fembed%2Fpicker%3Fmode%3Dsingle",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(authState.signinRedirect).toHaveBeenCalledTimes(1);
|
||||||
|
expect(authState.signinRedirect).toHaveBeenCalledWith({
|
||||||
|
state: {
|
||||||
|
returnTo: "/embed/picker?mode=single",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
cleanupRendered(rendered.container, rendered.root);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -30,7 +30,12 @@ function LoginPage() {
|
|||||||
if (!shouldAutoLogin) {
|
if (!shouldAutoLogin) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (autoStartedRef.current || auth.isLoading || auth.activeNavigator) {
|
if (
|
||||||
|
auth.isAuthenticated ||
|
||||||
|
autoStartedRef.current ||
|
||||||
|
auth.isLoading ||
|
||||||
|
auth.activeNavigator
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,7 +45,14 @@ function LoginPage() {
|
|||||||
returnTo,
|
returnTo,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}, [auth, auth.activeNavigator, auth.isLoading, returnTo, shouldAutoLogin]);
|
}, [
|
||||||
|
auth,
|
||||||
|
auth.activeNavigator,
|
||||||
|
auth.isAuthenticated,
|
||||||
|
auth.isLoading,
|
||||||
|
returnTo,
|
||||||
|
shouldAutoLogin,
|
||||||
|
]);
|
||||||
|
|
||||||
const handleSSOLogin = async () => {
|
const handleSSOLogin = async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user