1
0
forked from baron/baron-sso

fix: Admin UI 커스텀 필드 로그인 ID 반영 문제 및 비밀번호 초기화 동작 개선 (#440)

- 사용자 정보 수정(UpdateUser) 시 메타데이터(커스텀 필드)를 명시적 loginId 값보다 우선하여 동기화하도록 로직 순서 변경
- Admin UI 사용자 상세의 비밀번호 초기화 기능이 즉시 폼에 덮어씌워지는 문제 해결을 위해, 별도의 확인 절차 후 즉각 독립적인 API 호출을 통해 재설정되도록 개선
This commit is contained in:
2026-03-25 16:26:01 +09:00
parent d83646a7ef
commit 6a4c37603d
2 changed files with 70 additions and 31 deletions

View File

@@ -148,7 +148,8 @@ function UserDetailPage() {
const queryClient = useQueryClient();
const [error, setError] = React.useState<string | null>(null);
const [successMsg, setSuccessMsg] = React.useState<string | null>(null);
const [showPassword, setShowPassword] = React.useState(false);
const [isPasswordResetOpen, setIsPasswordResetOpen] = React.useState(false);
const [generatedPassword, setGeneratedPassword] = React.useState<string | null>(null);
const { data: profile } = useQuery({
queryKey: ["me"],
@@ -189,7 +190,6 @@ function UserDetailPage() {
department: "",
position: "",
jobTitle: "",
password: "",
metadata: {},
},
});
@@ -197,22 +197,38 @@ function UserDetailPage() {
const isAdmin =
profile?.role === "super_admin" || profile?.role === "tenant_admin";
const resetPasswordMutation = useMutation({
mutationFn: (newPass: string) => updateUser(userId, { password: newPass }),
onSuccess: (_, newPass) => {
setGeneratedPassword(newPass);
toast.success(
t(
"msg.admin.users.detail.password_generated",
"사용자 비밀번호가 성공적으로 재설정되었습니다.",
),
);
},
onError: (err: AxiosError<{ error?: string }>) => {
toast.error(
err.response?.data?.error ||
t("msg.admin.users.detail.update_error", "수정에 실패했습니다."),
);
},
});
const handleGeneratePassword = () => {
setIsPasswordResetOpen(true);
setGeneratedPassword(null);
};
const confirmGeneratePassword = () => {
const newPass = generateSecurePassword();
setValue("password", newPass);
setShowPassword(true);
toast.success(
t(
"msg.admin.users.detail.password_generated",
"안전한 비밀번호가 생성되었습니다.",
),
);
resetPasswordMutation.mutate(newPass);
};
const handleCopyPassword = () => {
const pass = watch("password");
if (pass) {
navigator.clipboard.writeText(pass);
if (generatedPassword) {
navigator.clipboard.writeText(generatedPassword);
toast.success(
t("msg.common.copied_to_clipboard", "클립보드에 복사되었습니다."),
);
@@ -670,14 +686,34 @@ function UserDetailPage() {
</Button>
</div>
{showPassword && (
{isPasswordResetOpen && !generatedPassword && (
<div className="p-4 border rounded-lg bg-destructive/5 space-y-4">
<p className="text-sm">
{t(
"msg.admin.users.detail.reset_password_confirm",
"정말로 이 사용자의 비밀번호를 초기화하시겠습니까? 기존 비밀번호로는 즉시 로그인할 수 없게 됩니다.",
)}
</p>
<div className="flex justify-end gap-2">
<Button variant="ghost" size="sm" onClick={() => setIsPasswordResetOpen(false)}>
{t("ui.common.cancel", "취소")}
</Button>
<Button variant="destructive" size="sm" onClick={confirmGeneratePassword} disabled={resetPasswordMutation.isPending}>
{resetPasswordMutation.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{t("ui.admin.users.detail.reset_password", "초기화 및 생성")}
</Button>
</div>
</div>
)}
{generatedPassword && (
<div className="p-4 border border-dashed rounded-lg bg-yellow-500/5 flex flex-wrap items-center justify-between gap-4">
<div className="space-y-1">
<p className="text-xs font-bold text-yellow-600 uppercase tracking-wider">
Generated Password
</p>
<p className="font-mono text-lg font-bold">
{watch("password")}
{generatedPassword}
</p>
</div>
<Button size="sm" variant="secondary" onClick={handleCopyPassword}>

View File

@@ -322,6 +322,11 @@ func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
"grade": role,
}
// [Override with explicit LoginID if provided]
if req.LoginID != "" {
attributes["id"] = req.LoginID
}
// [Resolve TenantID and LoginID before Kratos creation]
var tenantID string
if req.CompanyCode != "" && h.TenantService != nil {
@@ -343,11 +348,6 @@ func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
attributes["tenant_id"] = tenantID
}
// [Override with explicit LoginID if provided]
if req.LoginID != "" {
attributes["id"] = req.LoginID
}
// Merge custom metadata into attributes
for k, v := range req.Metadata {
// Don't overwrite core fields
@@ -558,7 +558,12 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
"role": role,
}
// Sync LoginID from configured custom field
// Override with explicit LoginID if provided
if item.LoginID != "" {
attributes["id"] = item.LoginID
}
// Sync LoginID from configured custom field (overrides explicit LoginID)
if tItem.LoginIDField != "" {
if val, exists := item.Metadata[tItem.LoginIDField]; exists {
if loginIDStr, ok := val.(string); ok && loginIDStr != "" {
@@ -567,11 +572,6 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
}
}
// Override with explicit LoginID if provided
if item.LoginID != "" {
attributes["id"] = item.LoginID
}
// Merge metadata
for k, v := range item.Metadata {
if _, exists := attributes[k]; !exists {
@@ -1151,6 +1151,14 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
traits["role"] = role
}
// [Override with explicit LoginID if provided]
// This is done FIRST so that if a custom loginIdField is configured in the tenant,
// the metadata sync below will override this explicit value, preventing the UI's
// pre-filled explicit loginId from clobbering the updated custom field.
if req.LoginID != nil && *req.LoginID != "" {
traits["id"] = *req.LoginID
}
// [LoginID Sync based on Tenant Settings]
schemaCompCode := extractTraitString(traits, "companyCode")
if req.CompanyCode != nil {
@@ -1177,11 +1185,6 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
}
}
// [Override with explicit LoginID if provided]
if req.LoginID != nil && *req.LoginID != "" {
traits["id"] = *req.LoginID
}
// [Namespaced Metadata Sync]
coreTraits := map[string]bool{
"email": true, "name": true, "phone_number": true,