forked from baron/baron-sso
feat: add schema tab access control and user password generator
This commit is contained in:
@@ -4,6 +4,10 @@ import {
|
||||
ArrowLeft,
|
||||
BadgeCheck,
|
||||
Building2,
|
||||
Copy,
|
||||
Dices,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Loader2,
|
||||
Save,
|
||||
Users,
|
||||
@@ -15,6 +19,7 @@ import {
|
||||
useForm,
|
||||
} from "react-hook-form";
|
||||
import { Link, useNavigate, useParams } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
@@ -36,6 +41,19 @@ import {
|
||||
} from "../../lib/adminApi";
|
||||
import { t } from "../../lib/i18n";
|
||||
|
||||
// Utility for secure password generation
|
||||
function generateSecurePassword(length = 16) {
|
||||
const charset =
|
||||
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+~`|}{[]:;?><,./-=";
|
||||
let retVal = "";
|
||||
const values = new Uint32Array(length);
|
||||
crypto.getRandomValues(values);
|
||||
for (let i = 0; i < length; i++) {
|
||||
retVal += charset.charAt(values[i] % charset.length);
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
type UserSchemaField = {
|
||||
key: string;
|
||||
label?: string;
|
||||
@@ -148,6 +166,7 @@ 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 { data: profile } = useQuery({
|
||||
queryKey: ["me"],
|
||||
@@ -175,6 +194,7 @@ function UserDetailPage() {
|
||||
handleSubmit,
|
||||
reset,
|
||||
watch,
|
||||
setValue,
|
||||
formState: { errors },
|
||||
} = useForm<UserFormValues>({
|
||||
defaultValues: {
|
||||
@@ -194,6 +214,28 @@ function UserDetailPage() {
|
||||
const isAdmin =
|
||||
profile?.role === "super_admin" || profile?.role === "tenant_admin";
|
||||
|
||||
const handleGeneratePassword = () => {
|
||||
const newPass = generateSecurePassword();
|
||||
setValue("password", newPass);
|
||||
setShowPassword(true);
|
||||
toast.success(
|
||||
t(
|
||||
"msg.admin.users.detail.password_generated",
|
||||
"안전한 비밀번호가 생성되었습니다.",
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const handleCopyPassword = () => {
|
||||
const pass = watch("password");
|
||||
if (pass) {
|
||||
navigator.clipboard.writeText(pass);
|
||||
toast.success(
|
||||
t("msg.common.copied_to_clipboard", "클립보드에 복사되었습니다."),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (user) {
|
||||
reset({
|
||||
@@ -556,15 +598,49 @@ function UserDetailPage() {
|
||||
"비밀번호 변경",
|
||||
)}
|
||||
</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder={t(
|
||||
"ui.admin.users.detail.security.password_placeholder",
|
||||
"변경할 경우에만 입력",
|
||||
)}
|
||||
{...register("password")}
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<div className="relative flex-1">
|
||||
<Input
|
||||
id="password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
placeholder={t(
|
||||
"ui.admin.users.detail.security.password_placeholder",
|
||||
"변경할 경우에만 입력",
|
||||
)}
|
||||
className="font-mono"
|
||||
{...register("password")}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||
</button>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleGeneratePassword}
|
||||
title={t(
|
||||
"ui.admin.users.detail.generate_password",
|
||||
"자동 생성",
|
||||
)}
|
||||
>
|
||||
<Dices size={16} className="mr-2" />
|
||||
{t("ui.common.generate", "생성")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={handleCopyPassword}
|
||||
disabled={!watch("password")}
|
||||
title={t("ui.common.copy", "복사")}
|
||||
>
|
||||
<Copy size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t(
|
||||
"msg.admin.users.detail.security.password_hint",
|
||||
|
||||
Reference in New Issue
Block a user