From 9e73059d2a4d305aff0c2f727859c00d169b4a91 Mon Sep 17 00:00:00 2001 From: kyy Date: Wed, 22 Apr 2026 15:40:17 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B0=9C=EB=B0=9C=EC=9E=90=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=20=EC=8B=A0=EC=B2=AD=20=EC=9E=85=EB=A0=A5=20=EC=95=88?= =?UTF-8?q?=EB=82=B4=20=EB=B0=8F=20=EC=97=AD=ED=95=A0=20=ED=91=9C=EA=B8=B0?= =?UTF-8?q?=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/domain/developer_request.go | 3 + backend/internal/handler/dev_handler.go | 36 ++++++- devfront/src/features/clients/ClientsPage.tsx | 93 ++++++++++++++-- .../DeveloperRequestPage.tsx | 101 ++++++++++++++++-- devfront/src/lib/devApi.ts | 3 + 5 files changed, 218 insertions(+), 18 deletions(-) diff --git a/backend/internal/domain/developer_request.go b/backend/internal/domain/developer_request.go index 22645924..58bfbc64 100644 --- a/backend/internal/domain/developer_request.go +++ b/backend/internal/domain/developer_request.go @@ -18,6 +18,9 @@ type DeveloperRequest struct { TenantID string `gorm:"index;not null" json:"tenantId"` Name string `gorm:"not null" json:"name"` Organization string `json:"organization"` + Email string `json:"email"` + Phone string `json:"phone"` + Role string `json:"role"` Reason string `json:"reason"` Status string `gorm:"default:'pending';not null" json:"status"` // pending, approved, rejected, cancelled AdminNotes string `json:"adminNotes"` diff --git a/backend/internal/handler/dev_handler.go b/backend/internal/handler/dev_handler.go index 1af7534e..6fcd134e 100644 --- a/backend/internal/handler/dev_handler.go +++ b/backend/internal/handler/dev_handler.go @@ -2800,7 +2800,17 @@ func (h *DevHandler) ListMyTenants(c *fiber.Ctx) error { role := normalizeUserRole(profile.Role) if role == domain.RoleUser { - return errorJSON(c, fiber.StatusForbidden, "access denied") + if profile.TenantID == nil || strings.TrimSpace(*profile.TenantID) == "" { + return c.JSON([]domain.Tenant{}) + } + tenant, err := h.TenantSvc.GetTenant(c.Context(), *profile.TenantID) + if err != nil { + return errorJSON(c, fiber.StatusInternalServerError, "failed to get tenant") + } + if tenant == nil { + return c.JSON([]domain.Tenant{}) + } + return c.JSON([]domain.Tenant{*tenant}) } if role == domain.RoleSuperAdmin { @@ -2839,6 +2849,12 @@ func (h *DevHandler) RequestDeveloperAccess(c *fiber.Ctx) error { if profile == nil { return errorJSON(c, fiber.StatusUnauthorized, "unauthorized") } + if h.Auth != nil { + if enriched, err := h.Auth.GetEnrichedProfile(c); err == nil && enriched != nil { + profile = enriched + c.Locals("user_profile", enriched) + } + } var req struct { Name string `json:"name"` @@ -2857,11 +2873,25 @@ func (h *DevHandler) RequestDeveloperAccess(c *fiber.Ctx) error { return errorJSON(c, fiber.StatusBadRequest, "tenantId is required") } + name := strings.TrimSpace(profile.Name) + if name == "" { + name = strings.TrimSpace(req.Name) + } + organization := strings.TrimSpace(req.Organization) + if h.TenantSvc != nil { + if tenant, err := h.TenantSvc.GetTenant(c.Context(), req.TenantID); err == nil && tenant != nil && strings.TrimSpace(tenant.Name) != "" { + organization = strings.TrimSpace(tenant.Name) + } + } + devReq := domain.DeveloperRequest{ UserID: profile.ID, TenantID: req.TenantID, - Name: req.Name, - Organization: req.Organization, + Name: name, + Organization: organization, + Email: profile.Email, + Phone: profile.Phone, + Role: normalizeUserRole(profile.Role), Reason: req.Reason, Status: domain.DeveloperRequestStatusPending, } diff --git a/devfront/src/features/clients/ClientsPage.tsx b/devfront/src/features/clients/ClientsPage.tsx index a00979a8..5dc148c0 100644 --- a/devfront/src/features/clients/ClientsPage.tsx +++ b/devfront/src/features/clients/ClientsPage.tsx @@ -9,7 +9,7 @@ import { ShieldHalf, X, } from "lucide-react"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { useAuth } from "react-oidc-context"; import { Link, useNavigate } from "react-router-dom"; import { ForbiddenMessage } from "../../components/common/ForbiddenMessage"; @@ -43,11 +43,13 @@ import { fetchClients, fetchDevStats, fetchDeveloperRequestStatus, + fetchMyTenants, requestDeveloperAccess, } from "../../lib/devApi"; import { t } from "../../lib/i18n"; import { resolveProfileRole } from "../../lib/role"; import { cn } from "../../lib/utils"; +import { fetchMe } from "../auth/authApi"; function ClientsPage() { const navigate = useNavigate(); @@ -56,6 +58,7 @@ function ClientsPage() { const userProfile = auth.user?.profile as Record | undefined; const role = resolveProfileRole(userProfile); const tenantId = userProfile?.tenant_id as string | undefined; + const companyCode = userProfile?.companyCode as string | undefined; const { data, @@ -82,6 +85,16 @@ function ClientsPage() { queryFn: () => fetchDeveloperRequestStatus(tenantId), enabled: hasAccessToken && role === "user", }); + const { data: tenants } = useQuery({ + queryKey: ["myTenants"], + queryFn: fetchMyTenants, + enabled: hasAccessToken, + }); + const { data: me } = useQuery({ + queryKey: ["userMe"], + queryFn: fetchMe, + enabled: hasAccessToken, + }); const canCreateClient = (role !== "user" && role !== "tenant_member") || @@ -117,6 +130,19 @@ function ClientsPage() { const authFailures = statsData?.auth_failures_24h ?? 0; const hasFilterResult = filteredClients.length > 0; const isFilteredOut = clients.length > 0 && !hasFilterResult; + const currentTenant = tenants?.find( + (tenant) => tenant.id === tenantId || tenant.slug === companyCode, + ); + const organizationName = currentTenant?.name || companyCode || ""; + const profileName = me?.name || (userProfile?.name as string) || ""; + const profileEmail = me?.email || (userProfile?.email as string) || ""; + const profilePhone = + me?.phone || + (userProfile?.phone as string | undefined) || + (userProfile?.phone_number as string | undefined) || + ""; + const profileRole = me?.role || role; + const profileRoleLabel = t(`ui.admin.role.${profileRole}`, profileRole); type StatTone = "up" | "down" | "stable"; type StatItem = { @@ -644,8 +670,11 @@ function ClientsPage() { setIsRequestModalOpen(false); }} tenantId={tenantId || ""} - initialName={(userProfile?.name as string) || ""} - initialOrg={(userProfile?.companyCode as string) || ""} + initialName={profileName} + initialOrg={organizationName} + initialEmail={profileEmail} + initialPhone={profilePhone} + initialRole={profileRoleLabel} /> ); @@ -658,6 +687,9 @@ interface RequestAccessModalProps { tenantId: string; initialName: string; initialOrg: string; + initialEmail: string; + initialPhone: string; + initialRole: string; } function RequestAccessModal({ @@ -667,11 +699,20 @@ function RequestAccessModal({ tenantId, initialName, initialOrg, + initialEmail, + initialPhone, + initialRole, }: RequestAccessModalProps) { const [name, setName] = useState(initialName); const [organization, setOrganization] = useState(initialOrg); const [reason, setReason] = useState(""); + useEffect(() => { + if (!isOpen) return; + setName(initialName); + setOrganization(initialOrg); + }, [initialName, initialOrg, isOpen]); + const mutation = useMutation({ mutationFn: requestDeveloperAccess, onSuccess: () => { @@ -725,7 +766,8 @@ function RequestAccessModal({ setName(e.target.value)} + readOnly + className="focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:border-input" required /> @@ -736,13 +778,50 @@ function RequestAccessModal({ setOrganization(e.target.value)} + readOnly + className="focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:border-input" required /> +
+
+ + +
+
+ + +
+
+
+ + +