forked from baron/baron-sso
본인 계정 비밀번호 초기화 기능 제한
This commit is contained in:
@@ -5,6 +5,8 @@ import {
|
|||||||
BadgeCheck,
|
BadgeCheck,
|
||||||
Building2,
|
Building2,
|
||||||
Copy,
|
Copy,
|
||||||
|
Eye,
|
||||||
|
EyeOff,
|
||||||
Key,
|
Key,
|
||||||
Loader2,
|
Loader2,
|
||||||
Mail,
|
Mail,
|
||||||
@@ -32,16 +34,19 @@ import {
|
|||||||
} from "../../components/ui/card";
|
} from "../../components/ui/card";
|
||||||
import { Input } from "../../components/ui/input";
|
import { Input } from "../../components/ui/input";
|
||||||
import { Label } from "../../components/ui/label";
|
import { Label } from "../../components/ui/label";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../components/ui/tabs";
|
||||||
import { toast } from "../../components/ui/use-toast";
|
import { toast } from "../../components/ui/use-toast";
|
||||||
import {
|
import {
|
||||||
type UserSummary,
|
type UserSummary,
|
||||||
type UserUpdateRequest,
|
type UserUpdateRequest,
|
||||||
deleteUser,
|
deleteUser,
|
||||||
|
fetchPasswordPolicy,
|
||||||
fetchMe,
|
fetchMe,
|
||||||
fetchTenants,
|
fetchTenants,
|
||||||
fetchUser,
|
fetchUser,
|
||||||
updateUser,
|
updateUser,
|
||||||
} from "../../lib/adminApi";
|
} from "../../lib/adminApi";
|
||||||
|
import type { PasswordPolicyResponse } from "../../lib/adminApi";
|
||||||
import { t } from "../../lib/i18n";
|
import { t } from "../../lib/i18n";
|
||||||
import { generateSecurePassword } from "../../lib/utils";
|
import { generateSecurePassword } from "../../lib/utils";
|
||||||
|
|
||||||
@@ -58,6 +63,116 @@ type UserFormValues = Omit<UserUpdateRequest, "metadata"> & {
|
|||||||
metadata: Record<string, Record<string, string | number | boolean>>;
|
metadata: Record<string, Record<string, string | number | boolean>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type PasswordResetMode = "generated" | "manual";
|
||||||
|
|
||||||
|
const PASSWORD_RESET_MIN_LENGTH = 12;
|
||||||
|
|
||||||
|
function buildPasswordPolicyDescription(policy?: PasswordPolicyResponse) {
|
||||||
|
const minLength = policy?.minLength ?? PASSWORD_RESET_MIN_LENGTH;
|
||||||
|
const minTypes = policy?.minCharacterTypes ?? 0;
|
||||||
|
const requiresLower = policy?.lowercase ?? true;
|
||||||
|
const requiresUpper = policy?.uppercase ?? false;
|
||||||
|
const requiresNumber = policy?.number ?? true;
|
||||||
|
const requiresSymbol = policy?.nonAlphanumeric ?? true;
|
||||||
|
|
||||||
|
const parts = [
|
||||||
|
t("msg.userfront.signup.policy.min_length", "최소 {{count}}자 이상", {
|
||||||
|
count: String(minLength),
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (minTypes > 0) {
|
||||||
|
parts.push(
|
||||||
|
t(
|
||||||
|
"msg.userfront.signup.policy.min_types",
|
||||||
|
"영문 대/소문자/숫자/특수문자 중 {{count}}가지 이상",
|
||||||
|
{ count: String(minTypes) },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (requiresLower) {
|
||||||
|
parts.push(t("msg.userfront.signup.policy.lowercase", "소문자"));
|
||||||
|
}
|
||||||
|
if (requiresUpper) {
|
||||||
|
parts.push(t("msg.userfront.signup.policy.uppercase", "대문자"));
|
||||||
|
}
|
||||||
|
if (requiresNumber) {
|
||||||
|
parts.push(t("msg.userfront.signup.policy.number", "숫자"));
|
||||||
|
}
|
||||||
|
if (requiresSymbol) {
|
||||||
|
parts.push(t("msg.userfront.signup.policy.symbol", "특수문자"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.join(", ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateManualPassword(
|
||||||
|
password: string,
|
||||||
|
policy?: PasswordPolicyResponse,
|
||||||
|
) {
|
||||||
|
if (password.trim().length === 0) {
|
||||||
|
return t(
|
||||||
|
"msg.admin.users.detail.password_manual_required",
|
||||||
|
"비밀번호를 입력해 주세요.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const minLength = policy?.minLength ?? PASSWORD_RESET_MIN_LENGTH;
|
||||||
|
if (password.length < minLength) {
|
||||||
|
return t(
|
||||||
|
"msg.userfront.reset.error.min_length",
|
||||||
|
"비밀번호는 최소 {{count}}자 이상이어야 합니다.",
|
||||||
|
{ count: String(minLength) },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasLower = /[a-z]/.test(password);
|
||||||
|
const hasUpper = /[A-Z]/.test(password);
|
||||||
|
const hasNumber = /[0-9]/.test(password);
|
||||||
|
const hasSymbol = /[\W_]/.test(password);
|
||||||
|
let typeCount = 0;
|
||||||
|
if (hasLower) typeCount++;
|
||||||
|
if (hasUpper) typeCount++;
|
||||||
|
if (hasNumber) typeCount++;
|
||||||
|
if (hasSymbol) typeCount++;
|
||||||
|
|
||||||
|
const minTypes = policy?.minCharacterTypes ?? 0;
|
||||||
|
if (minTypes > 0 && typeCount < minTypes) {
|
||||||
|
return t(
|
||||||
|
"msg.userfront.reset.error.min_types",
|
||||||
|
"비밀번호는 영문 대/소문자/숫자/특수문자 중 {{count}}가지 이상 포함해야 합니다.",
|
||||||
|
{ count: String(minTypes) },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((policy?.lowercase ?? true) && !hasLower) {
|
||||||
|
return t(
|
||||||
|
"msg.userfront.reset.error.lowercase",
|
||||||
|
"최소 1개 이상의 소문자를 포함해야 합니다.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if ((policy?.uppercase ?? false) && !hasUpper) {
|
||||||
|
return t(
|
||||||
|
"msg.userfront.reset.error.uppercase",
|
||||||
|
"최소 1개 이상의 대문자를 포함해야 합니다.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if ((policy?.number ?? true) && !hasNumber) {
|
||||||
|
return t(
|
||||||
|
"msg.userfront.reset.error.number",
|
||||||
|
"최소 1개 이상의 숫자를 포함해야 합니다.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if ((policy?.nonAlphanumeric ?? true) && !hasSymbol) {
|
||||||
|
return t(
|
||||||
|
"msg.userfront.reset.error.symbol",
|
||||||
|
"최소 1개 이상의 특수문자를 포함해야 합니다.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function TenantMetadataFields({
|
function TenantMetadataFields({
|
||||||
tenant,
|
tenant,
|
||||||
schema,
|
schema,
|
||||||
@@ -166,6 +281,15 @@ function UserDetailPage() {
|
|||||||
const [generatedPassword, setGeneratedPassword] = React.useState<
|
const [generatedPassword, setGeneratedPassword] = React.useState<
|
||||||
string | null
|
string | null
|
||||||
>(null);
|
>(null);
|
||||||
|
const [passwordResetMode, setPasswordResetMode] =
|
||||||
|
React.useState<PasswordResetMode>("generated");
|
||||||
|
const [manualPassword, setManualPassword] = React.useState("");
|
||||||
|
const [manualPasswordConfirm, setManualPasswordConfirm] = React.useState("");
|
||||||
|
const [isManualPasswordVisible, setIsManualPasswordVisible] =
|
||||||
|
React.useState(false);
|
||||||
|
const [passwordResetError, setPasswordResetError] = React.useState<
|
||||||
|
string | null
|
||||||
|
>(null);
|
||||||
|
|
||||||
const { data: profile } = useQuery({
|
const { data: profile } = useQuery({
|
||||||
queryKey: ["me"],
|
queryKey: ["me"],
|
||||||
@@ -187,6 +311,10 @@ function UserDetailPage() {
|
|||||||
queryFn: () => fetchTenants(100, 0),
|
queryFn: () => fetchTenants(100, 0),
|
||||||
});
|
});
|
||||||
const tenants = tenantsData?.items ?? [];
|
const tenants = tenantsData?.items ?? [];
|
||||||
|
const { data: passwordPolicy, isLoading: isPasswordPolicyLoading } = useQuery({
|
||||||
|
queryKey: ["password-policy"],
|
||||||
|
queryFn: fetchPasswordPolicy,
|
||||||
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
@@ -211,11 +339,13 @@ function UserDetailPage() {
|
|||||||
|
|
||||||
const isAdmin =
|
const isAdmin =
|
||||||
profile?.role === "super_admin" || profile?.role === "tenant_admin";
|
profile?.role === "super_admin" || profile?.role === "tenant_admin";
|
||||||
|
const isSelf = Boolean(profile?.id && user?.id && profile.id === user.id);
|
||||||
|
|
||||||
const resetPasswordMutation = useMutation({
|
const resetPasswordMutation = useMutation({
|
||||||
mutationFn: (newPass: string) => updateUser(userId, { password: newPass }),
|
mutationFn: (newPass: string) => updateUser(userId, { password: newPass }),
|
||||||
onSuccess: (_, newPass) => {
|
onSuccess: (_, newPass) => {
|
||||||
setGeneratedPassword(newPass);
|
setGeneratedPassword(newPass);
|
||||||
|
setPasswordResetError(null);
|
||||||
toast.success(
|
toast.success(
|
||||||
t(
|
t(
|
||||||
"msg.admin.users.detail.password_generated",
|
"msg.admin.users.detail.password_generated",
|
||||||
@@ -224,20 +354,67 @@ function UserDetailPage() {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
onError: (err: AxiosError<{ error?: string }>) => {
|
onError: (err: AxiosError<{ error?: string }>) => {
|
||||||
toast.error(
|
const message =
|
||||||
err.response?.data?.error ||
|
err.response?.data?.error ||
|
||||||
t("msg.admin.users.detail.update_error", "수정에 실패했습니다."),
|
t("msg.admin.users.detail.update_error", "수정에 실패했습니다.");
|
||||||
);
|
setPasswordResetError(message);
|
||||||
|
toast.error(message);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleGeneratePassword = () => {
|
const handleOpenPasswordReset = () => {
|
||||||
|
if (isSelf) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
setIsPasswordResetOpen(true);
|
setIsPasswordResetOpen(true);
|
||||||
setGeneratedPassword(null);
|
setGeneratedPassword(null);
|
||||||
|
setPasswordResetMode("generated");
|
||||||
|
setManualPassword("");
|
||||||
|
setManualPasswordConfirm("");
|
||||||
|
setIsManualPasswordVisible(false);
|
||||||
|
setPasswordResetError(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmGeneratePassword = () => {
|
const handleClosePasswordReset = () => {
|
||||||
const newPass = generateSecurePassword();
|
setIsPasswordResetOpen(false);
|
||||||
|
setGeneratedPassword(null);
|
||||||
|
setPasswordResetMode("generated");
|
||||||
|
setManualPassword("");
|
||||||
|
setManualPasswordConfirm("");
|
||||||
|
setIsManualPasswordVisible(false);
|
||||||
|
setPasswordResetError(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmPasswordReset = () => {
|
||||||
|
if (isSelf) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let newPass = manualPassword;
|
||||||
|
|
||||||
|
if (passwordResetMode === "manual") {
|
||||||
|
const validationError = validateManualPassword(
|
||||||
|
manualPassword,
|
||||||
|
passwordPolicy,
|
||||||
|
);
|
||||||
|
if (validationError) {
|
||||||
|
setPasswordResetError(validationError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (manualPassword !== manualPasswordConfirm) {
|
||||||
|
setPasswordResetError(
|
||||||
|
t(
|
||||||
|
"msg.userfront.reset.error.mismatch",
|
||||||
|
"비밀번호가 일치하지 않습니다.",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newPass = generateSecurePassword();
|
||||||
|
}
|
||||||
|
|
||||||
|
setPasswordResetError(null);
|
||||||
resetPasswordMutation.mutate(newPass);
|
resetPasswordMutation.mutate(newPass);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -717,7 +894,7 @@ function UserDetailPage() {
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<div className="flex items-center justify-between p-4 border rounded-lg bg-muted/20">
|
<div className="flex items-center justify-between rounded-lg border bg-muted/20 px-4 py-4">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<p className="text-sm font-medium">
|
<p className="text-sm font-medium">
|
||||||
{t(
|
{t(
|
||||||
@@ -726,44 +903,205 @@ function UserDetailPage() {
|
|||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
사용자의 비밀번호를 강제로 재설정하고 새 비밀번호를
|
{t(
|
||||||
생성합니다.
|
"msg.admin.users.detail.reset_password_help",
|
||||||
|
"사용자의 비밀번호를 강제로 재설정하고 자동 생성하거나 직접 입력한 비밀번호를 적용합니다.",
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline" onClick={handleGeneratePassword}>
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleOpenPasswordReset}
|
||||||
|
disabled={isSelf}
|
||||||
|
>
|
||||||
<RefreshCw className="mr-2 h-4 w-4" />
|
<RefreshCw className="mr-2 h-4 w-4" />
|
||||||
{t("ui.admin.users.detail.reset_password", "초기화 및 생성")}
|
{t("ui.admin.users.detail.reset_password", "초기화 및 설정")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isPasswordResetOpen && !generatedPassword && (
|
{isSelf && (
|
||||||
<div className="p-4 border rounded-lg bg-destructive/5 space-y-4">
|
<div className="rounded-lg border px-4 py-3">
|
||||||
<p className="text-sm">
|
<p className="text-sm text-muted-foreground">
|
||||||
{t(
|
{t(
|
||||||
"msg.admin.users.detail.reset_password_confirm",
|
"msg.admin.users.detail.self_password_reset_blocked",
|
||||||
"정말로 이 사용자의 비밀번호를 초기화하시겠습니까? 기존 비밀번호로는 즉시 로그인할 수 없게 됩니다.",
|
"본인 계정의 비밀번호는 사용자 포털(UserFront) 설정에서 변경해 주세요.",
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isPasswordResetOpen && !generatedPassword && !isSelf && (
|
||||||
|
<div className="space-y-4 rounded-lg border px-4 py-4">
|
||||||
|
<Tabs
|
||||||
|
value={passwordResetMode}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
setPasswordResetMode(value as PasswordResetMode);
|
||||||
|
setPasswordResetError(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TabsList className="grid w-full grid-cols-2 rounded-lg bg-muted p-1">
|
||||||
|
<TabsTrigger
|
||||||
|
value="generated"
|
||||||
|
className="bg-slate-200 font-normal text-foreground data-[state=inactive]:bg-slate-200 data-[state=active]:bg-background data-[state=active]:font-semibold data-[state=active]:text-foreground data-[state=active]:shadow-sm"
|
||||||
|
>
|
||||||
|
{t(
|
||||||
|
"ui.admin.users.detail.password_mode_generated",
|
||||||
|
"자동 생성",
|
||||||
|
)}
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger
|
||||||
|
value="manual"
|
||||||
|
className="bg-slate-200 font-normal text-foreground data-[state=inactive]:bg-slate-200 data-[state=active]:bg-background data-[state=active]:font-semibold data-[state=active]:text-foreground data-[state=active]:shadow-sm"
|
||||||
|
>
|
||||||
|
{t(
|
||||||
|
"ui.admin.users.detail.password_mode_manual",
|
||||||
|
"수동 입력",
|
||||||
|
)}
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<TabsContent value="generated" className="space-y-2">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{t(
|
||||||
|
"msg.admin.users.detail.password_generated_help",
|
||||||
|
"보안 기준에 맞는 임시 비밀번호를 자동 생성해 즉시 적용합니다.",
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="manual" className="space-y-4">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{isPasswordPolicyLoading
|
||||||
|
? t(
|
||||||
|
"msg.userfront.signup.policy.loading",
|
||||||
|
"비밀번호 정책을 불러오는 중입니다...",
|
||||||
|
)
|
||||||
|
: t(
|
||||||
|
"msg.userfront.signup.policy.summary",
|
||||||
|
"보안 정책: {{rules}}",
|
||||||
|
{
|
||||||
|
rules: buildPasswordPolicyDescription(
|
||||||
|
passwordPolicy,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<div className="relative flex-1">
|
||||||
|
<Input
|
||||||
|
id="manualPassword"
|
||||||
|
type={
|
||||||
|
isManualPasswordVisible ? "text" : "password"
|
||||||
|
}
|
||||||
|
value={manualPassword}
|
||||||
|
placeholder=" "
|
||||||
|
className="peer pr-12 pt-5"
|
||||||
|
onChange={(event) => {
|
||||||
|
setManualPassword(event.target.value);
|
||||||
|
if (passwordResetError) {
|
||||||
|
setPasswordResetError(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor="manualPassword"
|
||||||
|
className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 bg-background px-1 text-sm text-muted-foreground transition-all peer-placeholder-shown:top-1/2 peer-placeholder-shown:text-sm peer-focus:top-0 peer-focus:translate-y-[-50%] peer-focus:text-xs peer-[&:not(:placeholder-shown)]:top-0 peer-[&:not(:placeholder-shown)]:translate-y-[-50%] peer-[&:not(:placeholder-shown)]:text-xs"
|
||||||
|
>
|
||||||
|
{t("ui.userfront.reset.new_password", "새 비밀번호")}
|
||||||
|
</label>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="absolute right-1 top-1/2 h-8 w-8 -translate-y-1/2"
|
||||||
|
onClick={() =>
|
||||||
|
setIsManualPasswordVisible((prev) => !prev)
|
||||||
|
}
|
||||||
|
aria-label={t(
|
||||||
|
"ui.admin.users.detail.toggle_password_visibility",
|
||||||
|
"비밀번호 표시 전환",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isManualPasswordVisible ? (
|
||||||
|
<EyeOff className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<Eye className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<div className="relative flex-1">
|
||||||
|
<Input
|
||||||
|
id="manualPasswordConfirm"
|
||||||
|
type={
|
||||||
|
isManualPasswordVisible ? "text" : "password"
|
||||||
|
}
|
||||||
|
value={manualPasswordConfirm}
|
||||||
|
placeholder=" "
|
||||||
|
className="peer pr-12 pt-5"
|
||||||
|
onChange={(event) => {
|
||||||
|
setManualPasswordConfirm(event.target.value);
|
||||||
|
if (passwordResetError) {
|
||||||
|
setPasswordResetError(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor="manualPasswordConfirm"
|
||||||
|
className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 bg-background px-1 text-sm text-muted-foreground transition-all peer-placeholder-shown:top-1/2 peer-placeholder-shown:text-sm peer-focus:top-0 peer-focus:translate-y-[-50%] peer-focus:text-xs peer-[&:not(:placeholder-shown)]:top-0 peer-[&:not(:placeholder-shown)]:translate-y-[-50%] peer-[&:not(:placeholder-shown)]:text-xs"
|
||||||
|
>
|
||||||
|
{t(
|
||||||
|
"ui.userfront.reset.confirm_password",
|
||||||
|
"새 비밀번호 확인",
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="absolute right-1 top-1/2 h-8 w-8 -translate-y-1/2"
|
||||||
|
onClick={() =>
|
||||||
|
setIsManualPasswordVisible((prev) => !prev)
|
||||||
|
}
|
||||||
|
aria-label={t(
|
||||||
|
"ui.admin.users.detail.toggle_password_visibility",
|
||||||
|
"비밀번호 표시 전환",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isManualPasswordVisible ? (
|
||||||
|
<EyeOff className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<Eye className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
{passwordResetError && (
|
||||||
|
<p className="text-sm text-destructive">
|
||||||
|
{passwordResetError}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setIsPasswordResetOpen(false)}
|
onClick={handleClosePasswordReset}
|
||||||
>
|
>
|
||||||
{t("ui.common.cancel", "취소")}
|
{t("ui.common.cancel", "취소")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={confirmGeneratePassword}
|
onClick={confirmPasswordReset}
|
||||||
disabled={resetPasswordMutation.isPending}
|
disabled={resetPasswordMutation.isPending}
|
||||||
>
|
>
|
||||||
{resetPasswordMutation.isPending && (
|
{resetPasswordMutation.isPending && (
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
)}
|
)}
|
||||||
{t(
|
{t(
|
||||||
"ui.admin.users.detail.reset_password",
|
"ui.admin.users.detail.reset_password_apply",
|
||||||
"초기화 및 생성",
|
"비밀번호 적용",
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -774,7 +1112,10 @@ function UserDetailPage() {
|
|||||||
<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
|
{t(
|
||||||
|
"ui.admin.users.detail.password_result_title",
|
||||||
|
"Reset Password",
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
<p className="font-mono text-lg font-bold">
|
<p className="font-mono text-lg font-bold">
|
||||||
{generatedPassword}
|
{generatedPassword}
|
||||||
|
|||||||
@@ -288,6 +288,12 @@ name_required = "Name Required"
|
|||||||
|
|
||||||
[msg.admin.users.detail.security]
|
[msg.admin.users.detail.security]
|
||||||
password_hint = "Password Hint"
|
password_hint = "Password Hint"
|
||||||
|
password_generated_help = "Generate a temporary password that meets the security policy and apply it immediately."
|
||||||
|
password_manual_help = "Enter and apply a password with at least {{count}} characters."
|
||||||
|
password_manual_min_length = "Password must be at least {{count}} characters long."
|
||||||
|
password_manual_required = "Please enter a password."
|
||||||
|
reset_password_help = "Force-reset the user's password and apply either an auto-generated password or a manually entered one."
|
||||||
|
self_password_reset_blocked = "Please change your own password from the UserFront settings page."
|
||||||
|
|
||||||
[msg.admin.users.list]
|
[msg.admin.users.list]
|
||||||
delete_confirm = "Delete Confirm"
|
delete_confirm = "Delete Confirm"
|
||||||
@@ -1094,6 +1100,16 @@ password = "Password"
|
|||||||
password_placeholder = "Password Placeholder"
|
password_placeholder = "Password Placeholder"
|
||||||
title = "Security Settings"
|
title = "Security Settings"
|
||||||
|
|
||||||
|
[ui.admin.users.detail]
|
||||||
|
manual_password = "New Password"
|
||||||
|
manual_password_placeholder = "Enter a new password"
|
||||||
|
password_mode_generated = "Auto Generate"
|
||||||
|
password_mode_manual = "Manual Input"
|
||||||
|
password_result_title = "Reset Password"
|
||||||
|
reset_password = "Reset & Set"
|
||||||
|
reset_password_apply = "Apply Password"
|
||||||
|
toggle_password_visibility = "Toggle password visibility"
|
||||||
|
|
||||||
[ui.admin.users.detail.tenants_section]
|
[ui.admin.users.detail.tenants_section]
|
||||||
additional = "Additional Affiliated/Manageable Tenants"
|
additional = "Additional Affiliated/Manageable Tenants"
|
||||||
primary = "Representative Affiliated Tenant"
|
primary = "Representative Affiliated Tenant"
|
||||||
|
|||||||
@@ -288,6 +288,12 @@ name_required = "이름은 필수입니다."
|
|||||||
|
|
||||||
[msg.admin.users.detail.security]
|
[msg.admin.users.detail.security]
|
||||||
password_hint = "비밀번호를 변경하려면 입력하세요. 비워두면 현재 비밀번호가 유지됩니다."
|
password_hint = "비밀번호를 변경하려면 입력하세요. 비워두면 현재 비밀번호가 유지됩니다."
|
||||||
|
password_generated_help = "보안 기준에 맞는 임시 비밀번호를 자동 생성해 즉시 적용합니다."
|
||||||
|
password_manual_help = "최소 {{count}}자 이상의 비밀번호를 직접 입력해 적용합니다."
|
||||||
|
password_manual_min_length = "비밀번호는 최소 {{count}}자 이상이어야 합니다."
|
||||||
|
password_manual_required = "비밀번호를 입력해 주세요."
|
||||||
|
reset_password_help = "사용자의 비밀번호를 강제로 재설정하고 자동 생성하거나 직접 입력한 비밀번호를 적용합니다."
|
||||||
|
self_password_reset_blocked = "본인 계정의 비밀번호는 사용자 포털(UserFront) 설정에서 변경해 주세요."
|
||||||
|
|
||||||
[msg.admin.users.list]
|
[msg.admin.users.list]
|
||||||
delete_confirm = "사용자 \"{{name}}\"을(를) 정말 삭제하시겠습니까?"
|
delete_confirm = "사용자 \"{{name}}\"을(를) 정말 삭제하시겠습니까?"
|
||||||
@@ -1094,6 +1100,16 @@ password = "비밀번호 변경"
|
|||||||
password_placeholder = "변경할 경우에만 입력"
|
password_placeholder = "변경할 경우에만 입력"
|
||||||
title = "보안 설정"
|
title = "보안 설정"
|
||||||
|
|
||||||
|
[ui.admin.users.detail]
|
||||||
|
manual_password = "새 비밀번호"
|
||||||
|
manual_password_placeholder = "새 비밀번호를 입력하세요"
|
||||||
|
password_mode_generated = "자동 생성"
|
||||||
|
password_mode_manual = "수동 입력"
|
||||||
|
password_result_title = "Reset Password"
|
||||||
|
reset_password = "초기화 및 설정"
|
||||||
|
reset_password_apply = "비밀번호 적용"
|
||||||
|
toggle_password_visibility = "비밀번호 표시 전환"
|
||||||
|
|
||||||
[ui.admin.users.detail.tenants_section]
|
[ui.admin.users.detail.tenants_section]
|
||||||
additional = "추가 소속/관리 테넌트"
|
additional = "추가 소속/관리 테넌트"
|
||||||
primary = "대표 소속 테넌트"
|
primary = "대표 소속 테넌트"
|
||||||
|
|||||||
@@ -288,6 +288,12 @@ name_required = ""
|
|||||||
|
|
||||||
[msg.admin.users.detail.security]
|
[msg.admin.users.detail.security]
|
||||||
password_hint = ""
|
password_hint = ""
|
||||||
|
password_generated_help = ""
|
||||||
|
password_manual_help = ""
|
||||||
|
password_manual_min_length = ""
|
||||||
|
password_manual_required = ""
|
||||||
|
reset_password_help = ""
|
||||||
|
self_password_reset_blocked = ""
|
||||||
|
|
||||||
[msg.admin.users.list]
|
[msg.admin.users.list]
|
||||||
delete_confirm = ""
|
delete_confirm = ""
|
||||||
@@ -1094,6 +1100,16 @@ password = ""
|
|||||||
password_placeholder = ""
|
password_placeholder = ""
|
||||||
title = ""
|
title = ""
|
||||||
|
|
||||||
|
[ui.admin.users.detail]
|
||||||
|
manual_password = ""
|
||||||
|
manual_password_placeholder = ""
|
||||||
|
password_mode_generated = ""
|
||||||
|
password_mode_manual = ""
|
||||||
|
password_result_title = ""
|
||||||
|
reset_password = ""
|
||||||
|
reset_password_apply = ""
|
||||||
|
toggle_password_visibility = ""
|
||||||
|
|
||||||
[ui.admin.users.detail.tenants_section]
|
[ui.admin.users.detail.tenants_section]
|
||||||
additional = ""
|
additional = ""
|
||||||
primary = ""
|
primary = ""
|
||||||
|
|||||||
Reference in New Issue
Block a user