mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-15 20:25:23 +02:00
Merge pull request #3409 from mhbdev/auto-password-generator
Added a built-in password generator to the shared input
This commit is contained in:
@@ -559,6 +559,7 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
||||
type="password"
|
||||
placeholder="******************"
|
||||
autoComplete="one-time-code"
|
||||
enablePasswordGenerator={true}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
@@ -578,6 +579,7 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="******************"
|
||||
enablePasswordGenerator={true}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
@@ -10,7 +10,7 @@ export const ToggleVisibilityInput = ({ ...props }: InputProps) => {
|
||||
|
||||
return (
|
||||
<div className="flex w-full items-center space-x-2">
|
||||
<Input ref={inputRef} type={"password"} {...props} />
|
||||
<Input ref={inputRef} {...props} type="password" />
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
onClick={() => {
|
||||
|
||||
@@ -1,18 +1,74 @@
|
||||
import { EyeIcon, EyeOffIcon } from "lucide-react";
|
||||
import { EyeIcon, EyeOffIcon, RefreshCcw } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { generateRandomPassword } from "@/lib/password-utils";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
errorMessage?: string;
|
||||
enablePasswordGenerator?: boolean;
|
||||
passwordGeneratorLength?: number;
|
||||
}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, errorMessage, type, ...props }, ref) => {
|
||||
(
|
||||
{
|
||||
className,
|
||||
errorMessage,
|
||||
type,
|
||||
enablePasswordGenerator = false,
|
||||
passwordGeneratorLength,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const [showPassword, setShowPassword] = React.useState(false);
|
||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||
const isPassword = type === "password";
|
||||
const shouldShowGenerator =
|
||||
isPassword &&
|
||||
enablePasswordGenerator !== false &&
|
||||
!props.disabled &&
|
||||
!props.readOnly;
|
||||
const inputType = isPassword ? (showPassword ? "text" : "password") : type;
|
||||
|
||||
const setRefs = React.useCallback(
|
||||
(node: HTMLInputElement | null) => {
|
||||
inputRef.current = node;
|
||||
if (typeof ref === "function") {
|
||||
ref(node);
|
||||
} else if (ref) {
|
||||
ref.current = node;
|
||||
}
|
||||
},
|
||||
[ref],
|
||||
);
|
||||
|
||||
const handleGeneratePassword = () => {
|
||||
const nextValue =
|
||||
typeof passwordGeneratorLength === "number" &&
|
||||
passwordGeneratorLength > 0
|
||||
? generateRandomPassword(Math.floor(passwordGeneratorLength))
|
||||
: generateRandomPassword();
|
||||
|
||||
const input = inputRef.current;
|
||||
if (!input) {
|
||||
return;
|
||||
}
|
||||
|
||||
const valueSetter = Object.getOwnPropertyDescriptor(
|
||||
HTMLInputElement.prototype,
|
||||
"value",
|
||||
)?.set;
|
||||
if (valueSetter) {
|
||||
valueSetter.call(input, nextValue);
|
||||
} else {
|
||||
input.value = nextValue;
|
||||
}
|
||||
|
||||
input.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative w-full">
|
||||
@@ -21,25 +77,39 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
className={cn(
|
||||
// bg-gray
|
||||
"flex h-10 w-full rounded-md bg-input px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border disabled:cursor-not-allowed disabled:opacity-50",
|
||||
isPassword && "pr-10", // Add padding for the eye icon
|
||||
isPassword && (shouldShowGenerator ? "pr-16" : "pr-10"),
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
ref={setRefs}
|
||||
{...props}
|
||||
/>
|
||||
{isPassword && (
|
||||
<button
|
||||
type="button"
|
||||
className="absolute inset-y-0 right-0 flex items-center pr-3 text-muted-foreground hover:text-foreground focus:outline-none"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
tabIndex={-1}
|
||||
>
|
||||
{showPassword ? (
|
||||
<EyeOffIcon className="h-4 w-4" />
|
||||
) : (
|
||||
<EyeIcon className="h-4 w-4" />
|
||||
<div className="absolute inset-y-0 right-0 flex items-center gap-1 pr-3 text-muted-foreground">
|
||||
{shouldShowGenerator && (
|
||||
<button
|
||||
type="button"
|
||||
className="hover:text-foreground focus:outline-none"
|
||||
onClick={handleGeneratePassword}
|
||||
aria-label="Generate password"
|
||||
title="Generate password"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<RefreshCcw className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="hover:text-foreground focus:outline-none"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
tabIndex={-1}
|
||||
>
|
||||
{showPassword ? (
|
||||
<EyeOffIcon className="h-4 w-4" />
|
||||
) : (
|
||||
<EyeIcon className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{errorMessage && (
|
||||
|
||||
38
apps/dokploy/lib/password-utils.ts
Normal file
38
apps/dokploy/lib/password-utils.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
const DEFAULT_PASSWORD_LENGTH = 20;
|
||||
const DEFAULT_PASSWORD_CHARSET =
|
||||
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
|
||||
export const generateRandomPassword = (
|
||||
length: number = DEFAULT_PASSWORD_LENGTH,
|
||||
charset: string = DEFAULT_PASSWORD_CHARSET,
|
||||
) => {
|
||||
const safeLength =
|
||||
Number.isFinite(length) && length > 0
|
||||
? Math.floor(length)
|
||||
: DEFAULT_PASSWORD_LENGTH;
|
||||
|
||||
if (safeLength <= 0 || charset.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const cryptoApi =
|
||||
typeof globalThis !== "undefined" ? globalThis.crypto : undefined;
|
||||
|
||||
if (!cryptoApi?.getRandomValues) {
|
||||
let fallback = "";
|
||||
for (let i = 0; i < safeLength; i += 1) {
|
||||
fallback += charset[Math.floor(Math.random() * charset.length)];
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
const values = new Uint32Array(safeLength);
|
||||
cryptoApi.getRandomValues(values);
|
||||
|
||||
let result = "";
|
||||
for (const value of values) {
|
||||
result += charset[value % charset.length];
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
Reference in New Issue
Block a user