forked from baron/baron-sso
fix: Admin UI 커스텀 필드 로그인 ID 반영 문제 및 비밀번호 초기화 동작 개선 (#440)
- 사용자 정보 수정(UpdateUser) 시 메타데이터(커스텀 필드)를 명시적 loginId 값보다 우선하여 동기화하도록 로직 순서 변경 - Admin UI 사용자 상세의 비밀번호 초기화 기능이 즉시 폼에 덮어씌워지는 문제 해결을 위해, 별도의 확인 절차 후 즉각 독립적인 API 호출을 통해 재설정되도록 개선
This commit is contained in:
@@ -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}>
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user