forked from baron/baron-sso
본인 계정 비밀번호 초기화 기능 제한
This commit is contained in:
@@ -5,6 +5,8 @@ import {
|
||||
BadgeCheck,
|
||||
Building2,
|
||||
Copy,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Key,
|
||||
Loader2,
|
||||
Mail,
|
||||
@@ -32,16 +34,19 @@ import {
|
||||
} from "../../components/ui/card";
|
||||
import { Input } from "../../components/ui/input";
|
||||
import { Label } from "../../components/ui/label";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../components/ui/tabs";
|
||||
import { toast } from "../../components/ui/use-toast";
|
||||
import {
|
||||
type UserSummary,
|
||||
type UserUpdateRequest,
|
||||
deleteUser,
|
||||
fetchPasswordPolicy,
|
||||
fetchMe,
|
||||
fetchTenants,
|
||||
fetchUser,
|
||||
updateUser,
|
||||
} from "../../lib/adminApi";
|
||||
import type { PasswordPolicyResponse } from "../../lib/adminApi";
|
||||
import { t } from "../../lib/i18n";
|
||||
import { generateSecurePassword } from "../../lib/utils";
|
||||
|
||||
@@ -58,6 +63,116 @@ type UserFormValues = Omit<UserUpdateRequest, "metadata"> & {
|
||||
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({
|
||||
tenant,
|
||||
schema,
|
||||
@@ -166,6 +281,15 @@ function UserDetailPage() {
|
||||
const [generatedPassword, setGeneratedPassword] = React.useState<
|
||||
string | 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({
|
||||
queryKey: ["me"],
|
||||
@@ -187,6 +311,10 @@ function UserDetailPage() {
|
||||
queryFn: () => fetchTenants(100, 0),
|
||||
});
|
||||
const tenants = tenantsData?.items ?? [];
|
||||
const { data: passwordPolicy, isLoading: isPasswordPolicyLoading } = useQuery({
|
||||
queryKey: ["password-policy"],
|
||||
queryFn: fetchPasswordPolicy,
|
||||
});
|
||||
|
||||
const {
|
||||
register,
|
||||
@@ -211,11 +339,13 @@ function UserDetailPage() {
|
||||
|
||||
const isAdmin =
|
||||
profile?.role === "super_admin" || profile?.role === "tenant_admin";
|
||||
const isSelf = Boolean(profile?.id && user?.id && profile.id === user.id);
|
||||
|
||||
const resetPasswordMutation = useMutation({
|
||||
mutationFn: (newPass: string) => updateUser(userId, { password: newPass }),
|
||||
onSuccess: (_, newPass) => {
|
||||
setGeneratedPassword(newPass);
|
||||
setPasswordResetError(null);
|
||||
toast.success(
|
||||
t(
|
||||
"msg.admin.users.detail.password_generated",
|
||||
@@ -224,20 +354,67 @@ function UserDetailPage() {
|
||||
);
|
||||
},
|
||||
onError: (err: AxiosError<{ error?: string }>) => {
|
||||
toast.error(
|
||||
const message =
|
||||
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);
|
||||
setGeneratedPassword(null);
|
||||
setPasswordResetMode("generated");
|
||||
setManualPassword("");
|
||||
setManualPasswordConfirm("");
|
||||
setIsManualPasswordVisible(false);
|
||||
setPasswordResetError(null);
|
||||
};
|
||||
|
||||
const confirmGeneratePassword = () => {
|
||||
const newPass = generateSecurePassword();
|
||||
const handleClosePasswordReset = () => {
|
||||
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);
|
||||
};
|
||||
|
||||
@@ -717,7 +894,7 @@ function UserDetailPage() {
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<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">
|
||||
<p className="text-sm font-medium">
|
||||
{t(
|
||||
@@ -726,44 +903,205 @@ function UserDetailPage() {
|
||||
)}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
사용자의 비밀번호를 강제로 재설정하고 새 비밀번호를
|
||||
생성합니다.
|
||||
{t(
|
||||
"msg.admin.users.detail.reset_password_help",
|
||||
"사용자의 비밀번호를 강제로 재설정하고 자동 생성하거나 직접 입력한 비밀번호를 적용합니다.",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="outline" onClick={handleGeneratePassword}>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleOpenPasswordReset}
|
||||
disabled={isSelf}
|
||||
>
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
{t("ui.admin.users.detail.reset_password", "초기화 및 생성")}
|
||||
{t("ui.admin.users.detail.reset_password", "초기화 및 설정")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{isPasswordResetOpen && !generatedPassword && (
|
||||
<div className="p-4 border rounded-lg bg-destructive/5 space-y-4">
|
||||
<p className="text-sm">
|
||||
{isSelf && (
|
||||
<div className="rounded-lg border px-4 py-3">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t(
|
||||
"msg.admin.users.detail.reset_password_confirm",
|
||||
"정말로 이 사용자의 비밀번호를 초기화하시겠습니까? 기존 비밀번호로는 즉시 로그인할 수 없게 됩니다.",
|
||||
"msg.admin.users.detail.self_password_reset_blocked",
|
||||
"본인 계정의 비밀번호는 사용자 포털(UserFront) 설정에서 변경해 주세요.",
|
||||
)}
|
||||
</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">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setIsPasswordResetOpen(false)}
|
||||
onClick={handleClosePasswordReset}
|
||||
>
|
||||
{t("ui.common.cancel", "취소")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={confirmGeneratePassword}
|
||||
onClick={confirmPasswordReset}
|
||||
disabled={resetPasswordMutation.isPending}
|
||||
>
|
||||
{resetPasswordMutation.isPending && (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
)}
|
||||
{t(
|
||||
"ui.admin.users.detail.reset_password",
|
||||
"초기화 및 생성",
|
||||
"ui.admin.users.detail.reset_password_apply",
|
||||
"비밀번호 적용",
|
||||
)}
|
||||
</Button>
|
||||
</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="space-y-1">
|
||||
<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 className="font-mono text-lg font-bold">
|
||||
{generatedPassword}
|
||||
|
||||
@@ -288,6 +288,12 @@ name_required = "Name Required"
|
||||
|
||||
[msg.admin.users.detail.security]
|
||||
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]
|
||||
delete_confirm = "Delete Confirm"
|
||||
@@ -1094,6 +1100,16 @@ password = "Password"
|
||||
password_placeholder = "Password Placeholder"
|
||||
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]
|
||||
additional = "Additional Affiliated/Manageable Tenants"
|
||||
primary = "Representative Affiliated Tenant"
|
||||
|
||||
@@ -288,6 +288,12 @@ name_required = "이름은 필수입니다."
|
||||
|
||||
[msg.admin.users.detail.security]
|
||||
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]
|
||||
delete_confirm = "사용자 \"{{name}}\"을(를) 정말 삭제하시겠습니까?"
|
||||
@@ -1094,6 +1100,16 @@ password = "비밀번호 변경"
|
||||
password_placeholder = "변경할 경우에만 입력"
|
||||
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]
|
||||
additional = "추가 소속/관리 테넌트"
|
||||
primary = "대표 소속 테넌트"
|
||||
|
||||
@@ -288,6 +288,12 @@ name_required = ""
|
||||
|
||||
[msg.admin.users.detail.security]
|
||||
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]
|
||||
delete_confirm = ""
|
||||
@@ -1094,6 +1100,16 @@ password = ""
|
||||
password_placeholder = ""
|
||||
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]
|
||||
additional = ""
|
||||
primary = ""
|
||||
|
||||
Reference in New Issue
Block a user