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 queryClient = useQueryClient();
|
||||||
const [error, setError] = React.useState<string | null>(null);
|
const [error, setError] = React.useState<string | null>(null);
|
||||||
const [successMsg, setSuccessMsg] = 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({
|
const { data: profile } = useQuery({
|
||||||
queryKey: ["me"],
|
queryKey: ["me"],
|
||||||
@@ -189,7 +190,6 @@ function UserDetailPage() {
|
|||||||
department: "",
|
department: "",
|
||||||
position: "",
|
position: "",
|
||||||
jobTitle: "",
|
jobTitle: "",
|
||||||
password: "",
|
|
||||||
metadata: {},
|
metadata: {},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -197,22 +197,38 @@ function UserDetailPage() {
|
|||||||
const isAdmin =
|
const isAdmin =
|
||||||
profile?.role === "super_admin" || profile?.role === "tenant_admin";
|
profile?.role === "super_admin" || profile?.role === "tenant_admin";
|
||||||
|
|
||||||
const handleGeneratePassword = () => {
|
const resetPasswordMutation = useMutation({
|
||||||
const newPass = generateSecurePassword();
|
mutationFn: (newPass: string) => updateUser(userId, { password: newPass }),
|
||||||
setValue("password", newPass);
|
onSuccess: (_, newPass) => {
|
||||||
setShowPassword(true);
|
setGeneratedPassword(newPass);
|
||||||
toast.success(
|
toast.success(
|
||||||
t(
|
t(
|
||||||
"msg.admin.users.detail.password_generated",
|
"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();
|
||||||
|
resetPasswordMutation.mutate(newPass);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCopyPassword = () => {
|
const handleCopyPassword = () => {
|
||||||
const pass = watch("password");
|
if (generatedPassword) {
|
||||||
if (pass) {
|
navigator.clipboard.writeText(generatedPassword);
|
||||||
navigator.clipboard.writeText(pass);
|
|
||||||
toast.success(
|
toast.success(
|
||||||
t("msg.common.copied_to_clipboard", "클립보드에 복사되었습니다."),
|
t("msg.common.copied_to_clipboard", "클립보드에 복사되었습니다."),
|
||||||
);
|
);
|
||||||
@@ -670,14 +686,34 @@ function UserDetailPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</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="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">
|
<div className="space-y-1">
|
||||||
<p className="text-xs font-bold text-yellow-600 uppercase tracking-wider">
|
<p className="text-xs font-bold text-yellow-600 uppercase tracking-wider">
|
||||||
Generated Password
|
Generated Password
|
||||||
</p>
|
</p>
|
||||||
<p className="font-mono text-lg font-bold">
|
<p className="font-mono text-lg font-bold">
|
||||||
{watch("password")}
|
{generatedPassword}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button size="sm" variant="secondary" onClick={handleCopyPassword}>
|
<Button size="sm" variant="secondary" onClick={handleCopyPassword}>
|
||||||
|
|||||||
@@ -322,6 +322,11 @@ func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
|
|||||||
"grade": role,
|
"grade": role,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [Override with explicit LoginID if provided]
|
||||||
|
if req.LoginID != "" {
|
||||||
|
attributes["id"] = req.LoginID
|
||||||
|
}
|
||||||
|
|
||||||
// [Resolve TenantID and LoginID before Kratos creation]
|
// [Resolve TenantID and LoginID before Kratos creation]
|
||||||
var tenantID string
|
var tenantID string
|
||||||
if req.CompanyCode != "" && h.TenantService != nil {
|
if req.CompanyCode != "" && h.TenantService != nil {
|
||||||
@@ -343,11 +348,6 @@ func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
|
|||||||
attributes["tenant_id"] = tenantID
|
attributes["tenant_id"] = tenantID
|
||||||
}
|
}
|
||||||
|
|
||||||
// [Override with explicit LoginID if provided]
|
|
||||||
if req.LoginID != "" {
|
|
||||||
attributes["id"] = req.LoginID
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge custom metadata into attributes
|
// Merge custom metadata into attributes
|
||||||
for k, v := range req.Metadata {
|
for k, v := range req.Metadata {
|
||||||
// Don't overwrite core fields
|
// Don't overwrite core fields
|
||||||
@@ -558,7 +558,12 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
|||||||
"role": role,
|
"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 tItem.LoginIDField != "" {
|
||||||
if val, exists := item.Metadata[tItem.LoginIDField]; exists {
|
if val, exists := item.Metadata[tItem.LoginIDField]; exists {
|
||||||
if loginIDStr, ok := val.(string); ok && loginIDStr != "" {
|
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
|
// Merge metadata
|
||||||
for k, v := range item.Metadata {
|
for k, v := range item.Metadata {
|
||||||
if _, exists := attributes[k]; !exists {
|
if _, exists := attributes[k]; !exists {
|
||||||
@@ -1151,6 +1151,14 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
|
|||||||
traits["role"] = role
|
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]
|
// [LoginID Sync based on Tenant Settings]
|
||||||
schemaCompCode := extractTraitString(traits, "companyCode")
|
schemaCompCode := extractTraitString(traits, "companyCode")
|
||||||
if req.CompanyCode != nil {
|
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]
|
// [Namespaced Metadata Sync]
|
||||||
coreTraits := map[string]bool{
|
coreTraits := map[string]bool{
|
||||||
"email": true, "name": true, "phone_number": true,
|
"email": true, "name": true, "phone_number": true,
|
||||||
|
|||||||
Reference in New Issue
Block a user