From 95a2730e7126342df17f08772d70cea5b76baf6e Mon Sep 17 00:00:00 2001 From: chan Date: Tue, 16 Jun 2026 16:56:03 +0900 Subject: [PATCH] =?UTF-8?q?backend:=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85?= =?UTF-8?q?=20=EC=8B=9C=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=EB=8C=80=EC=A1=B0?= =?UTF-8?q?=EB=A5=BC=20=ED=86=B5=ED=95=9C=20=EA=B0=80=EC=A1=B1=EC=82=AC(AF?= =?UTF-8?q?FILIATE)=20=EA=B0=95=EC=A0=9C=20=EB=A1=9C=EC=A7=81=EC=9D=84=20?= =?UTF-8?q?=EC=A0=84=EB=A9=B4=20=EC=A0=9C=EA=B1=B0=ED=95=98=EA=B3=A0=20?= =?UTF-8?q?=EA=B8=B0=EB=B3=B8=20=EA=B0=9C=EC=9D=B8(Personal)=20=EA=B0=80?= =?UTF-8?q?=EC=9E=85=EC=9C=BC=EB=A1=9C=20=ED=86=B5=ED=95=A9=20(#1183)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/handler/auth_handler.go | 70 +++---------------- .../handler/auth_handler_signup_test.go | 10 +-- 2 files changed, 12 insertions(+), 68 deletions(-) diff --git a/backend/internal/handler/auth_handler.go b/backend/internal/handler/auth_handler.go index 541db7b3..a9379f0a 100644 --- a/backend/internal/handler/auth_handler.go +++ b/backend/internal/handler/auth_handler.go @@ -712,69 +712,17 @@ func (h *AuthHandler) Signup(c *fiber.Ctx) error { } // 소속이 비어 있는 일반 가입자는 PERSONAL tenant를 자동 생성해 대표소속을 보장합니다. - tenantSlug := strings.TrimSpace(req.TenantSlug) + // 모든 온라인 가입자는 기본적으로 개인(Personal) 테넌트 소속으로 가입합니다. + // 기업/가족사 소속 연동은 별도 문의를 통해 처리되므로 온라인 가입 흐름에서는 제외합니다. + req.AffiliationType = "GENERAL" + slog.Info("[Signup] Forcing AffiliationType to GENERAL (Default personal tenant signup policy)", "email", req.Email) + var tenantID *string - - parts := strings.Split(req.Email, "@") - if len(parts) != 2 { - return errorJSON(c, fiber.StatusBadRequest, "Invalid email format") - } - domainName := parts[1] - - // Check if this domain belongs to a predefined family affiliate - isInternal, _ := h.isAffiliateTenant(c.Context(), domainName) - - // [Strict Policy] Force AffiliationType based on predefined family slugs (User cannot choose) - if isInternal { - req.AffiliationType = "AFFILIATE" - slog.Info("[Signup] Forcing AffiliationType to AFFILIATE", "email", req.Email) - } else { - req.AffiliationType = "GENERAL" - slog.Info("[Signup] Forcing AffiliationType to GENERAL", "email", req.Email) - } - - if tenantSlug != "" { - // [Security] Cross-check: If domain is NOT internal, they cannot provide a tenantSlug - if !isInternal { - slog.Warn("[Signup] Security violation: non-internal email providing tenantSlug", "email", req.Email) - return errorJSON(c, fiber.StatusForbidden, "Only affiliate members can join an organization.") - } - - if !affiliateSlugs[strings.ToLower(tenantSlug)] { - return errorJSON(c, fiber.StatusForbidden, "The selected organization is not a valid family affiliate.") - } - - tenant, err := h.TenantService.GetTenantBySlug(c.Context(), tenantSlug) - if err == nil && tenant != nil { - if tenant.Status == domain.TenantStatusActive { - slog.Info("[Signup] Assigning tenant by manual slug", "email", req.Email, "tenant", tenant.Slug) - tenantSlug = tenant.Slug - tenantID = &tenant.ID - } else { - return errorJSON(c, fiber.StatusForbidden, "The specified organization is not active.") - } - } else { - slog.Warn("[Signup] Attempted to join non-existent organization", "slug", tenantSlug, "email", req.Email) - return errorJSON(c, fiber.StatusNotFound, "The specified organization code was not found.") - } - } else { - // If it's a family affiliate domain, they MUST select one of the family companies - if isInternal { - return errorJSON(c, fiber.StatusBadRequest, "Please select your organization.") - } - } - - if tenantID == nil && req.AffiliationType == "AFFILIATE" { - return errorJSON(c, fiber.StatusBadRequest, "We couldn't verify your organization affiliation. Please check your choice.") - } - if tenantID == nil && req.AffiliationType == "GENERAL" { - tenant, err := createPersonalTenantForUser(c.Context(), h.TenantService, req.Email) - if err != nil { - return errorJSON(c, fiber.StatusServiceUnavailable, "failed to create personal tenant") - } - tenantSlug = tenant.Slug - tenantID = &tenant.ID + tenant, err := createPersonalTenantForUser(c.Context(), h.TenantService, req.Email) + if err != nil { + return errorJSON(c, fiber.StatusServiceUnavailable, "failed to create personal tenant") } + tenantID = &tenant.ID // Normalize Phone (E.164 형태로 보관) normalizedPhone := domain.NormalizePhoneNumber(req.Phone) diff --git a/backend/internal/handler/auth_handler_signup_test.go b/backend/internal/handler/auth_handler_signup_test.go index 0ee23240..2fdd4ea9 100644 --- a/backend/internal/handler/auth_handler_signup_test.go +++ b/backend/internal/handler/auth_handler_signup_test.go @@ -116,22 +116,18 @@ func TestSignup_TenantSlugValidation(t *testing.T) { assert.Equal(t, http.StatusBadRequest, resp.StatusCode) }) - t.Run("Active Tenant Slug", func(t *testing.T) { + t.Run("Success creates Personal Tenant", func(t *testing.T) { reqBody := domain.SignupRequest{ Email: "user@hanmaceng.co.kr", Password: "StrongPass123!", Name: "Test User", Phone: "010-1234-5678", TermsAccepted: true, - TenantSlug: "hanmac", } body, _ := json.Marshal(reqBody) - validTenant := &domain.Tenant{ID: "t1", Slug: "hanmac", Status: domain.TenantStatusActive} - mockTenantSvc.On("GetTenantByDomain", mock.Anything, "hanmaceng.co.kr").Return(&domain.Tenant{Slug: "hanmac"}, nil).Once() - mockTenantSvc.On("ProvisionTenantByDomain", mock.Anything, "hanmaceng.co.kr").Return(validTenant, nil).Maybe() - mockTenantSvc.On("GetTenantBySlug", mock.Anything, "hanmac").Return(validTenant, nil).Once() - mockTenantSvc.On("GetTenant", mock.Anything, "t1").Return(validTenant, nil).Once() + validTenant := &domain.Tenant{ID: "personal-t1", Slug: "personal-slug", Status: domain.TenantStatusActive} + mockTenantSvc.On("RegisterTenant", mock.Anything, "Personal - user@hanmaceng.co.kr", mock.Anything, domain.TenantTypePersonal, "Automatically provisioned personal tenant", []string(nil), (*string)(nil), "").Return(validTenant, nil).Once() mockIdp.On("CreateUser", mock.Anything, mock.Anything).Return("user-id", nil).Once() mockRedis.On("Delete", mock.Anything).Return(nil)