1
0
forked from baron/baron-sso

make drop 초기화 추가. 한맥그룹 기본값 추가

This commit is contained in:
2026-04-27 17:51:46 +09:00
parent 3fe32b1dfe
commit 08aa745e30
8 changed files with 334 additions and 18 deletions

View File

@@ -11,6 +11,12 @@ COMPOSE_INFRA := compose.infra.yaml
COMPOSE_ORY := compose.ory.yaml
COMPOSE_APP := docker-compose.yaml
AUTH_CONFIG_ENV := .generated/auth-config.env
DEV_SERVICES ?= backend adminfront devfront orgfront userfront
DEV_NETWORKS := baron_net ory-net hydranet kratosnet public_net
INFRA_CONTAINERS := baron_postgres baron_clickhouse baron_redis baron_gateway
ORY_CONTAINERS := ory_postgres ory_kratos ory_hydra ory_keto ory_oathkeeper ory_clickhouse ory_vector
APP_CONTAINERS := baron_backend baron_adminfront baron_devfront baron_orgfront baron_userfront
DROP_CONTAINERS := $(INFRA_CONTAINERS) $(ORY_CONTAINERS) $(APP_CONTAINERS) ory_stack_check
COMPOSE_CLI_ENV_ARGS :=
ifneq (,$(wildcard ./.env))
@@ -18,6 +24,13 @@ COMPOSE_CLI_ENV_ARGS += --env-file .env
endif
COMPOSE_CLI_ENV_ARGS += --env-file $(AUTH_CONFIG_ENV)
COMPOSE_DROP_ENV_ARGS :=
ifneq (,$(wildcard ./.env))
COMPOSE_DROP_ENV_ARGS += --env-file .env
endif
.PHONY: build-auth-config validate-auth-config verify-auth-config up-all up-infra up-ory up-app up-backend ensure-networks ensure-infra ensure-ory up-dev up-front-dev dev down drop down-app down-backend down-infra down-ory check-infra ps logs-infra logs-ory logs-app
# --- 인증 설정 빌드/검증 ---
build-auth-config:
@echo "Building auth config..."
@@ -34,38 +47,94 @@ verify-auth-config: validate-auth-config
# --- 기본 실행 ---
# 주의: --remove-orphan 사용 금지 (다른 스택이 orphan으로 판단되어 종료될 수 있음)
up-all: validate-auth-config
up-all: ensure-networks validate-auth-config
@echo "Starting ALL stacks (infra + ory + app)..."
docker compose $(COMPOSE_CLI_ENV_ARGS) -f $(COMPOSE_INFRA) -f $(COMPOSE_ORY) -f $(COMPOSE_APP) up -d
# --- 개별 스택 실행 ---
up-infra:
up-infra: ensure-networks
@echo "Starting Infra stack (postgres/clickhouse/redis)..."
docker compose -f $(COMPOSE_INFRA) up -d
up-ory: validate-auth-config
up-ory: ensure-networks validate-auth-config
@echo "Starting Ory stack (kratos/hydra/keto/oathkeeper)..."
docker compose $(COMPOSE_CLI_ENV_ARGS) -f $(COMPOSE_ORY) up -d
up-app: validate-auth-config
up-app: ensure-networks validate-auth-config
@echo "Starting App stack (backend/userfront/adminfront/devfront)..."
docker compose $(COMPOSE_CLI_ENV_ARGS) -f $(COMPOSE_APP) up -d
up-backend: validate-auth-config
up-backend: ensure-networks validate-auth-config
@echo "Starting Backend only..."
docker compose $(COMPOSE_CLI_ENV_ARGS) -f $(COMPOSE_APP) up -d backend
up-dev: up-infra up-ory
ensure-networks:
@echo "Ensuring Docker networks..."
@for network in $(DEV_NETWORKS); do \
if ! docker network inspect "$$network" >/dev/null 2>&1; then \
echo "Creating Docker network $$network..."; \
docker network create "$$network"; \
else \
echo "Docker network $$network already exists."; \
fi; \
done
ensure-infra: ensure-networks
@echo "Ensuring Infra stack..."
@missing=0; \
for container in $(INFRA_CONTAINERS); do \
if [ "$$(docker inspect -f '{{.State.Running}}' "$$container" 2>/dev/null)" != "true" ]; then \
missing=1; \
break; \
fi; \
done; \
if [ "$$missing" -eq 1 ]; then \
echo "Starting missing Infra stack containers in daemon mode..."; \
docker compose -f $(COMPOSE_INFRA) up -d; \
else \
echo "Infra stack is already running."; \
fi
ensure-ory: ensure-networks validate-auth-config
@echo "Ensuring Ory stack..."
@missing=0; \
for container in $(ORY_CONTAINERS); do \
if [ "$$(docker inspect -f '{{.State.Running}}' "$$container" 2>/dev/null)" != "true" ]; then \
missing=1; \
break; \
fi; \
done; \
if [ "$$missing" -eq 1 ]; then \
echo "Starting missing Ory stack containers in daemon mode..."; \
docker compose $(COMPOSE_CLI_ENV_ARGS) -f $(COMPOSE_ORY) up -d; \
else \
echo "Ory stack is already running."; \
fi
up-dev: ensure-infra ensure-ory
@echo "Dev stack is up (infra + ory)."
up-front-dev: up-infra up-ory up-backend
@echo "Dev stack is up (infra + ory + backend)."
dev: up-dev
@echo "Starting development app containers in foreground attach mode..."
docker compose $(COMPOSE_CLI_ENV_ARGS) -f $(COMPOSE_APP) up $(DEV_SERVICES)
# --- 종료 (Down) ---
down:
@echo "Stopping ALL stacks (infra + ory + app)..."
docker compose -f $(COMPOSE_INFRA) -f $(COMPOSE_ORY) -f $(COMPOSE_APP) down
drop:
@echo "Dropping Baron SSO local Docker stack containers, volumes, and local images..."
-docker compose $(COMPOSE_DROP_ENV_ARGS) -f $(COMPOSE_INFRA) -f $(COMPOSE_ORY) -f $(COMPOSE_APP) down -v --rmi local
@echo "Removing any remaining fixed-name Baron SSO containers..."
@for container in $(DROP_CONTAINERS); do \
docker rm -f "$$container" >/dev/null 2>&1 || true; \
done
@echo "Drop complete. External Docker networks are preserved."
down-app:
@echo "Stopping App stack..."
docker compose -f $(COMPOSE_APP) down

View File

@@ -111,7 +111,7 @@ test2@example.com,이몽룡,삼안,선임,개발,팀원,기술본부,개발실,
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "org_chart_template.csv";
a.download = "org_user_import_template.csv";
a.click();
URL.revokeObjectURL(url);
};
@@ -130,18 +130,18 @@ test2@example.com,이몽룡,삼안,선임,개발,팀원,기술본부,개발실,
<DialogTrigger asChild>
<Button variant="outline" size="sm" className="gap-2">
<Upload size={14} />
{t("ui.admin.org.import_btn", "조직도 임포트 (CSV/XLSX)")}
{t("ui.admin.org.import_btn", "조직/사용자 통합 임포트")}
</Button>
</DialogTrigger>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>
{t("ui.admin.org.import_title", "조직 일괄 등록")}
{t("ui.admin.org.import_title", "조직/사용자 통합 일괄 등록")}
</DialogTitle>
<DialogDescription>
{t(
"msg.admin.org.import_description",
"CSV 또는 XLSX 파일을 업로드하여 조직 계층과 멤버를 한 번에 구성합니다.",
"CSV 또는 XLSX 파일을 업로드하여 조직 테넌트와 사용자를 함께 생성/업데이트하고 멤버십을 매핑합니다.",
)}
</DialogDescription>
</DialogHeader>

View File

@@ -159,7 +159,7 @@ scope = "Scope"
[msg.admin.org]
hover_member_info = "Hover to see member details."
import_description = "Upload a CSV file to bulk register the organization chart."
import_description = "Upload a CSV or XLSX file to create or update organization tenants and users together, then map memberships."
import_error = "An error occurred during organization chart import."
import_success = "Organization chart imported successfully."
@@ -841,8 +841,8 @@ users = "Users"
[ui.admin.org]
download_template = "Download Template"
import_btn = "Import"
import_title = "Bulk Organization Import"
import_btn = "Org/User Import"
import_title = "Bulk Org/User Import"
start_import = "Start Import"
[ui.admin.overview]

View File

@@ -159,7 +159,7 @@ scope = "관리 기능은 /admin 네임스페이스에서만 노출합니다."
[msg.admin.org]
hover_member_info = "마우스를 올리면 상세 정보를 확인할 수 있습니다."
import_description = "CSV 또는 XLSX 파일을 업로드하여 조직도를 일괄 등록합니다. (필수 컬럼: 이메일, 이름)"
import_description = "CSV 또는 XLSX 파일을 업로드하여 조직 테넌트와 사용자를 함께 생성/업데이트하고 멤버십을 매핑합니다. (필수 컬럼: 이메일, 이름)"
import_error = "조직도 임포트 중 오류가 발생했습니다."
import_success = "조직도가 성공적으로 임포트되었습니다."
@@ -843,8 +843,8 @@ users = "사용자"
[ui.admin.org]
download_template = "템플릿 다운로드"
import_btn = "임포트"
import_title = "조직도 대량 등록"
import_btn = "조직/사용자 통합 임포트"
import_title = "조직/사용자 통합 일괄 등록"
start_import = "임포트 시작"
[ui.admin.overview]

View File

@@ -114,6 +114,26 @@ test.describe("Tenants Management", () => {
await expect(page).toHaveURL(/.*\/tenants$/, { timeout: 15000 });
});
test("should clarify organization import creates org tenants and users together", async ({
page,
}) => {
await page.goto("/tenants");
await expect(page.locator("h2").last()).toContainText(
/테넌트 목록|Tenants/i,
{ timeout: 20000 },
);
await page
.locator("button")
.filter({ hasText: /임포트|Import/i })
.first()
.click();
await expect(page.getByRole("dialog")).toContainText(
/조직 테넌트.*사용자|organization tenants.*users/i,
);
});
test("should show validation error on empty name", async ({ page }) => {
await page.goto("/tenants/new");
await expect(page.locator("h2").last()).toContainText(/추가|Create/i, {

View File

@@ -5,6 +5,8 @@ import (
"baron-sso-backend/internal/repository"
"baron-sso-backend/internal/service"
"context"
"errors"
"fmt"
"log/slog"
"gorm.io/gorm"
@@ -13,6 +15,8 @@ import (
type InitialTenantConfig struct {
Name string
Slug string
Type string
ParentSlug string
Description string
Domains []string
}
@@ -20,11 +24,25 @@ type InitialTenantConfig struct {
// Hardcoded for now, can be moved to config file or env later
var defaultTenants = []InitialTenantConfig{
{
Name: "Hanmac Engineering",
Name: "한맥가족",
Slug: "hanmac-family",
Type: domain.TenantTypeCompanyGroup,
},
{
Name: "한맥기술",
Slug: "hanmac",
Type: domain.TenantTypeCompany,
ParentSlug: "hanmac-family",
Description: "Primary Family Company",
Domains: []string{"hanmaceng.co.kr", "hmac.kr"},
},
{
Name: "삼안",
Slug: "saman",
Type: domain.TenantTypeCompany,
ParentSlug: "hanmac-family",
Domains: []string{"samaneng.com"},
},
}
func SeedTenants(db *gorm.DB) error {
@@ -37,9 +55,65 @@ func SeedTenants(db *gorm.DB) error {
ctx := context.Background()
for _, config := range defaultTenants {
tenantType := config.Type
if tenantType == "" {
tenantType = domain.TenantTypeCompany
}
var parentID *string
if config.ParentSlug != "" {
parent, err := repo.FindBySlug(ctx, config.ParentSlug)
if err != nil || parent == nil {
if err == nil {
err = errors.New("parent tenant not found")
}
slog.Error("Failed to resolve parent tenant for seed", "slug", config.Slug, "parentSlug", config.ParentSlug, "error", err)
return fmt.Errorf("resolve parent tenant %q for seed %q: %w", config.ParentSlug, config.Slug, err)
}
parentID = &parent.ID
}
existing, err := repo.FindBySlug(ctx, config.Slug)
if err == nil && existing != nil {
slog.Info("[Bootstrap] Tenant already exists, checking domains...", "slug", config.Slug)
changed := false
if existing.Name != config.Name {
existing.Name = config.Name
changed = true
}
if existing.Type != tenantType {
existing.Type = tenantType
changed = true
}
if existing.Status != domain.TenantStatusActive {
existing.Status = domain.TenantStatusActive
changed = true
}
if config.ParentSlug != "" {
if existing.ParentID == nil || *existing.ParentID != *parentID {
existing.ParentID = parentID
changed = true
if err := outboxRepo.Create(ctx, &domain.KetoOutbox{
Namespace: "Tenant",
Object: existing.ID,
Relation: "parents",
Subject: "Tenant:" + *parentID,
Action: domain.KetoOutboxActionCreate,
}); err != nil {
slog.Error("Failed to create outbox entry for seeded tenant hierarchy", "tenant", existing.ID, "error", err)
return err
}
}
} else if existing.ParentID != nil {
existing.ParentID = nil
changed = true
}
if changed {
if err := repo.Update(ctx, existing); err != nil {
slog.Error("Failed to update seeded tenant", "slug", config.Slug, "error", err)
return err
}
}
// Optional: Check and add missing domains
for _, d := range config.Domains {
found := false
@@ -60,7 +134,7 @@ func SeedTenants(db *gorm.DB) error {
}
slog.Info("[Bootstrap] Creating default tenant", "name", config.Name, "slug", config.Slug)
tenant, err := svc.RegisterTenant(ctx, config.Name, config.Slug, domain.TenantTypeCompany, config.Description, config.Domains, nil, "")
tenant, err := svc.RegisterTenant(ctx, config.Name, config.Slug, tenantType, config.Description, config.Domains, parentID, "")
if err != nil {
slog.Error("Failed to seed tenant", "slug", config.Slug, "error", err)
return err

View File

@@ -0,0 +1,74 @@
package bootstrap
import (
"baron-sso-backend/internal/domain"
"reflect"
"testing"
)
func TestDefaultTenantsSeedOrderAndHierarchy(t *testing.T) {
expected := []struct {
name string
slug string
tenantType string
parentSlug string
domains []string
}{
{
name: "한맥가족",
slug: "hanmac-family",
tenantType: domain.TenantTypeCompanyGroup,
},
{
name: "한맥기술",
slug: "hanmac",
tenantType: domain.TenantTypeCompany,
parentSlug: "hanmac-family",
domains: []string{"hanmaceng.co.kr", "hmac.kr"},
},
{
name: "삼안",
slug: "saman",
tenantType: domain.TenantTypeCompany,
parentSlug: "hanmac-family",
domains: []string{"samaneng.com"},
},
}
if len(defaultTenants) != len(expected) {
t.Fatalf("expected %d default tenants, got %d", len(expected), len(defaultTenants))
}
for i, want := range expected {
got := defaultTenants[i]
if got.Name != want.name {
t.Fatalf("tenant[%d] name = %q, want %q", i, got.Name, want.name)
}
if got.Slug != want.slug {
t.Fatalf("tenant[%d] slug = %q, want %q", i, got.Slug, want.slug)
}
if tenantType := stringField(t, got, "Type"); tenantType != want.tenantType {
t.Fatalf("tenant[%d] type = %q, want %q", i, tenantType, want.tenantType)
}
if parentSlug := stringField(t, got, "ParentSlug"); parentSlug != want.parentSlug {
t.Fatalf("tenant[%d] parent slug = %q, want %q", i, parentSlug, want.parentSlug)
}
if !reflect.DeepEqual(got.Domains, want.domains) {
t.Fatalf("tenant[%d] domains = %#v, want %#v", i, got.Domains, want.domains)
}
}
}
func stringField(t *testing.T, target InitialTenantConfig, name string) string {
t.Helper()
value := reflect.ValueOf(target)
field := value.FieldByName(name)
if !field.IsValid() {
t.Fatalf("InitialTenantConfig.%s is required", name)
}
if field.Kind() != reflect.String {
t.Fatalf("InitialTenantConfig.%s must be a string", name)
}
return field.String()
}

View File

@@ -0,0 +1,79 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
dry_run_dev="$(
make --dry-run --always-make -C "$repo_root" dev DEV_SERVICES="backend adminfront" 2>&1
)"
if ! grep -q "Ensuring Infra stack" <<<"$dry_run_dev"; then
echo "make dev must ensure the infra stack first." >&2
exit 1
fi
if ! grep -q "Ensuring Ory stack" <<<"$dry_run_dev"; then
echo "make dev must ensure the Ory stack first." >&2
exit 1
fi
app_up_line="$(
grep -E "docker compose .* -f docker-compose.yaml up .*backend.*adminfront" <<<"$dry_run_dev" | tail -1
)"
if [[ -z "$app_up_line" ]]; then
echo "make dev must run docker compose up for development app services." >&2
exit 1
fi
if grep -q -- " -d" <<<"$app_up_line"; then
echo "make dev must run app services in foreground attach mode without -d." >&2
exit 1
fi
dry_run_up_dev="$(
make --dry-run --always-make -C "$repo_root" up-dev 2>&1
)"
if ! grep -q "Ensuring Infra stack" <<<"$dry_run_up_dev"; then
echo "make up-dev must ensure the infra stack." >&2
exit 1
fi
if ! grep -q "Ensuring Ory stack" <<<"$dry_run_up_dev"; then
echo "make up-dev must ensure the Ory stack." >&2
exit 1
fi
dry_run_up_all="$(
make --dry-run --always-make -C "$repo_root" up-all 2>&1
)"
if ! grep -q "Ensuring Docker networks" <<<"$dry_run_up_all"; then
echo "make up-all must ensure external Docker networks before compose up." >&2
exit 1
fi
if ! grep -q 'docker network create "$network"' <<<"$dry_run_up_all"; then
echo "make up-all must create missing external Docker networks." >&2
exit 1
fi
dry_run_drop="$(
make --dry-run --always-make -C "$repo_root" drop 2>&1
)"
if ! grep -q "Dropping Baron SSO local Docker stack" <<<"$dry_run_drop"; then
echo "make drop must announce that it is dropping the local stack." >&2
exit 1
fi
if ! grep -q -- "down -v --rmi local" <<<"$dry_run_drop"; then
echo "make drop must remove containers, volumes, and local compose images." >&2
exit 1
fi
if ! grep -q "docker rm -f" <<<"$dry_run_drop"; then
echo "make drop must force-remove known fixed-name stack containers." >&2
exit 1
fi