feat(reset-password): implement reset password functionality for admins and customers

- Added new ResetPasswordModalContent component for displaying generated passwords.
- Integrated reset password functionality in the AdminsTable and CustomersTable components, allowing users to reset passwords for admins and customers.
- Created useResetPasswordModalPresenter hook to manage password visibility and clipboard copying.
- Updated API services and hooks to support reset password requests for both admins and customers.
- Enhanced tooltip functionality for reset password actions in the tables.
This commit is contained in:
AmirReza Jamali
2026-05-30 12:09:57 +03:30
parent 746107e977
commit 5ef0298840
16 changed files with 480 additions and 9 deletions
+43 -1
View File
@@ -1,6 +1,11 @@
"use client";
import { PencilSquareIcon, TrashIcon, UserSettingIcon } from "@/assets/icons";
import {
KeyIcon,
PencilSquareIcon,
TrashIcon,
UserSettingIcon,
} from "@/assets/icons";
import EmptyListWrapper from "@/components/HOC/EmptyListWrapper";
import ConfirmationModal from "@/components/ui/ConfirmationModal";
import IsVisible from "@/components/ui/IsVisible";
@@ -8,6 +13,7 @@ import ListHeader from "@/components/ui/ListHeader";
import ListWrapper from "@/components/ui/ListWrapper";
import Modal from "@/components/ui/modal";
import Pagination from "@/components/ui/pagination";
import ResetPasswordModalContent from "@/components/ui/ResetPasswordModalContent";
import Status from "@/components/ui/Status";
import {
Table,
@@ -56,6 +62,11 @@ const AdminsTable = () => {
navigateToEditAdmin,
openDeleteModalForUser,
openChangeStatusModalForUser,
isResetPasswordModalOpen,
closeResetPasswordModal,
openResetPasswordModalForUser,
generatedPassword,
isResettingPassword,
} = useAdminsPresenter();
return (
@@ -153,6 +164,23 @@ const AdminsTable = () => {
<span className="sr-only">Change Admin Status</span>
<UserSettingIcon />
</button>
<button
type="button"
data-tooltip-id="reset-password"
data-tooltip-content="Reset password"
data-tooltip-place="top"
className="hover:text-primary"
data-cy={`admins-row-reset-password-${item?.id ?? index}`}
onClick={() => openResetPasswordModalForUser(item)}
>
<Tooltip
{..._TooltipDefaultParams({
id: "reset-password",
})}
/>
<span className="sr-only">Reset Admin Password</span>
<KeyIcon />
</button>
<button
type="button"
data-tooltip-id="edit"
@@ -190,6 +218,20 @@ const AdminsTable = () => {
/>
</Modal>
<Modal
isOpen={isResetPasswordModalOpen}
onClose={closeResetPasswordModal}
showButtons={false}
>
<ResetPasswordModalContent
subjectName={currentUser?.full_name}
subjectLabel="admin"
password={generatedPassword}
isLoading={isResettingPassword}
isOpen={isResetPasswordModalOpen}
/>
</Modal>
{isModalOpen && (
<ConfirmationModal
isOpen={isModalOpen}
@@ -6,6 +6,7 @@ import {
useChangeAdminStatus,
useDeleteAdmin,
useGetAdminListQuery,
useResetAdminPassword,
} from "@/hooks/queries";
import { IUser, TChangeAdminStatusCredentials } from "@/lib/api";
import { useHybridPagination } from "@/hooks/useHybridPagination";
@@ -27,6 +28,11 @@ export const useAdminsPresenter = () => {
const [isChangeStatusModalOpen, setIsChangeStatusModalOpen] = useState(false);
const [currentUser, setCurrentUser] = useState<IUser | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const [isResetPasswordModalOpen, setIsResetPasswordModalOpen] =
useState(false);
const [generatedPassword, setGeneratedPassword] = useState<string | null>(
null,
);
const [status, setStatus] = useState<string>(
currentUser?.status ?? AdminStatus.ACTIVE,
);
@@ -115,6 +121,26 @@ export const useAdminsPresenter = () => {
setIsChangeStatusModalOpen(true);
}, []);
const { mutate: resetAdminPassword, isPending: isResettingPassword } =
useResetAdminPassword({
successCallback: (password) => setGeneratedPassword(password),
});
const openResetPasswordModalForUser = useCallback(
(user: IUser) => {
setCurrentUser(user);
setGeneratedPassword(null);
setIsResetPasswordModalOpen(true);
resetAdminPassword(user.id);
},
[resetAdminPassword],
);
const closeResetPasswordModal = useCallback(() => {
setIsResetPasswordModalOpen(false);
setGeneratedPassword(null);
}, []);
useLayoutEffect(() => {
const reduced =
typeof window !== "undefined" &&
@@ -237,5 +263,10 @@ export const useAdminsPresenter = () => {
navigateToEditAdmin,
openDeleteModalForUser,
openChangeStatusModalForUser,
isResetPasswordModalOpen,
closeResetPasswordModal,
openResetPasswordModalForUser,
generatedPassword,
isResettingPassword,
};
};
+45 -1
View File
@@ -1,5 +1,10 @@
"use client";
import { ExclamationIcon, PencilSquareIcon, TrashIcon } from "@/assets/icons";
import {
ExclamationIcon,
KeyIcon,
PencilSquareIcon,
TrashIcon,
} from "@/assets/icons";
import { Building } from "@/components/Layouts/sidebar/icons";
import EmptyListWrapper from "@/components/HOC/EmptyListWrapper";
import ConfirmationModal from "@/components/ui/ConfirmationModal";
@@ -8,6 +13,7 @@ import ListHeader from "@/components/ui/ListHeader";
import ListWrapper from "@/components/ui/ListWrapper";
import Modal from "@/components/ui/modal";
import Pagination from "@/components/ui/pagination";
import ResetPasswordModalContent from "@/components/ui/ResetPasswordModalContent";
import Status from "@/components/ui/Status";
import {
Table,
@@ -58,6 +64,11 @@ const CustomersTable = () => {
dataTbodyRef,
pagination,
handlePaginationChange,
isResetPasswordModalOpen,
closeResetPasswordModal,
openResetPasswordModalForCustomer,
generatedPassword,
isResettingPassword,
} = useCustomerListPresenter();
return (
@@ -215,6 +226,26 @@ const CustomersTable = () => {
<span className="sr-only">Assign To Company </span>
<Building />
</button>
<button
type="button"
data-tooltip-id="reset-password"
data-tooltip-content="Reset password"
data-tooltip-place="top"
className="hover:text-primary"
onClick={() =>
openResetPasswordModalForCustomer(customer)
}
>
<Tooltip
{..._TooltipDefaultParams({
id: "reset-password",
})}
/>
<span className="sr-only">
Reset Customer Password
</span>
<KeyIcon />
</button>
<button
data-tooltip-id="feedback"
data-tooltip-content="Feedback"
@@ -269,6 +300,19 @@ const CustomersTable = () => {
/>
</Modal>
</div>
<Modal
isOpen={isResetPasswordModalOpen}
onClose={closeResetPasswordModal}
showButtons={false}
>
<ResetPasswordModalContent
subjectName={currentCustomer?.full_name}
subjectLabel="customer"
password={generatedPassword}
isLoading={isResettingPassword}
isOpen={isResetPasswordModalOpen}
/>
</Modal>
{isModalOpen && (
<ConfirmationModal
isOpen={isModalOpen}
@@ -5,6 +5,7 @@ import {
useCompanyFullList,
useCustomersQuery,
useDeleteCustomerQuery,
useResetCustomerPassword,
} from "@/hooks/queries";
import { useHybridPagination } from "@/hooks/useHybridPagination";
import { useTableSectionRowAnimations } from "@/hooks/useTableSectionRowAnimations";
@@ -27,6 +28,11 @@ const useCustomerListPresenter = () => {
);
const [isModalOpen, setIsModalOpen] = useState(false);
const [isResetPasswordModalOpen, setIsResetPasswordModalOpen] =
useState(false);
const [generatedPassword, setGeneratedPassword] = useState<string | null>(
null,
);
const pathName = usePathname();
const searchParams = useSearchParams();
const [params, setParams] = useState<Record<string, any>>({
@@ -90,6 +96,26 @@ const useCustomerListPresenter = () => {
},
);
const { mutate: resetCustomerPassword, isPending: isResettingPassword } =
useResetCustomerPassword({
successCallback: (password) => setGeneratedPassword(password),
});
const openResetPasswordModalForCustomer = useCallback(
(customer: TCustomer) => {
setCurrentCustomer(customer);
setGeneratedPassword(null);
setIsResetPasswordModalOpen(true);
resetCustomerPassword(customer.id);
},
[resetCustomerPassword],
);
const closeResetPasswordModal = useCallback(() => {
setIsResetPasswordModalOpen(false);
setGeneratedPassword(null);
}, []);
const search = (formData: any) => {
const newParams = buildFirstPageParams({ ...params, ...formData });
const cleanedParams = deleteEmptyKeys(newParams);
@@ -154,6 +180,11 @@ const useCustomerListPresenter = () => {
dataTbodyRef,
pagination,
handlePaginationChange,
isResetPasswordModalOpen,
closeResetPasswordModal,
openResetPasswordModalForCustomer,
generatedPassword,
isResettingPassword,
};
};
@@ -119,6 +119,8 @@ const parseCpvCodesFromSearchParams = (
const buildTenderFilterFormValues = (params: Record<string, any>) => ({
...params,
documents_scraped:
params.documents_scraped === true || params.documents_scraped === "true",
cpv_codes: formatCpvCodesForForm(params.cpv_codes),
created_at_range: buildRangeFormValue(
params,
@@ -157,6 +159,7 @@ const TENDER_FILTER_FORM_DEFAULTS: Record<string, any> = {
main_classification: "",
classifications: "",
cpv_codes: "",
documents_scraped: false,
created_at_range: [],
tender_deadline_range: [],
publication_date_range: [],
@@ -0,0 +1,128 @@
"use client";
import { CheckIcon, CopyIcon, EyeIcon, KeyIcon } from "@/assets/icons";
import { cn } from "@/lib/utils";
import { FC } from "react";
import IsVisible from "./IsVisible";
import { useResetPasswordModalPresenter } from "./useResetPasswordModalPresenter";
interface IProps {
subjectName?: string | null;
subjectLabel?: string;
password: string | null;
isLoading: boolean;
isOpen: boolean;
}
const ResetPasswordModalContent: FC<IProps> = ({
subjectName,
subjectLabel = "user",
password,
isLoading,
isOpen,
}) => {
const {
isCopied,
isPasswordVisible,
togglePasswordVisibility,
copyPasswordToClipboard,
} = useResetPasswordModalPresenter({ isOpen, password });
return (
<div
className="w-full max-w-md sm:min-w-[24rem]"
data-cy="reset-password-modal-content"
>
<div className="relative mb-5 flex flex-col items-center text-center">
<div className="pointer-events-none absolute -top-6 h-24 w-24 rounded-full bg-primary/25 blur-3xl" />
<div className="relative flex h-16 w-16 items-center justify-center rounded-full bg-gradient-to-br from-primary/15 via-primary/10 to-primary/5 ring-1 ring-inset ring-primary/20 shadow-inner">
<KeyIcon className="h-7 w-7 text-primary" />
</div>
<h3 className="mt-4 text-lg font-semibold tracking-tight text-dark dark:text-white">
Reset Password
</h3>
<p className="mt-1 max-w-xs text-sm leading-relaxed text-dark-5 dark:text-dark-6">
<IsVisible condition={!!subjectName}>
A new password has been generated for{" "}
<span className="font-medium text-dark dark:text-white">
{subjectName}
</span>
. Copy it now it will not be shown again.
</IsVisible>
<IsVisible condition={!subjectName}>
Generating a new password for this {subjectLabel}. Copy it now it
will not be shown again.
</IsVisible>
</p>
</div>
<IsVisible condition={isLoading}>
<div
className="flex h-24 items-center justify-center rounded-xl bg-gray-1/60 ring-1 ring-inset ring-stroke dark:bg-dark/40 dark:ring-stroke-dark"
data-cy="reset-password-loading"
>
<span className="inline-flex items-center gap-2 text-sm font-medium text-dark-5 dark:text-dark-6">
<span className="h-4 w-4 animate-spin rounded-full border-2 border-primary/30 border-t-primary" />
Generating a secure password
</span>
</div>
</IsVisible>
<IsVisible condition={!isLoading && !!password}>
<div
className="group relative overflow-hidden rounded-xl bg-gradient-to-br from-gray-1 via-white to-gray-1 p-4 ring-1 ring-inset ring-stroke shadow-sm dark:from-dark/40 dark:via-dark-2 dark:to-dark/40 dark:ring-stroke-dark"
data-cy="reset-password-result"
>
<p className="mb-2 text-xs font-semibold uppercase tracking-wider text-dark-5 dark:text-dark-6">
New password
</p>
<div className="flex items-center gap-2">
<code
className="flex-1 select-all rounded-lg bg-white px-3 py-2.5 font-mono text-sm font-semibold tracking-wide text-dark shadow-inner ring-1 ring-inset ring-stroke break-all dark:bg-dark-3 dark:text-white dark:ring-stroke-dark"
data-cy="reset-password-value"
>
{isPasswordVisible
? password
: "•".repeat(Math.max(password?.length ?? 0, 8))}
</code>
<button
type="button"
onClick={togglePasswordVisibility}
className="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-white text-dark-5 ring-1 ring-inset ring-stroke transition hover:-translate-y-0.5 hover:text-primary dark:bg-dark-3 dark:text-dark-6 dark:ring-stroke-dark dark:hover:text-primary"
data-cy="reset-password-toggle-visibility"
aria-label={
isPasswordVisible ? "Hide password" : "Show password"
}
>
<EyeIcon className="h-5 w-5" />
</button>
</div>
<button
type="button"
onClick={copyPasswordToClipboard}
disabled={isCopied}
className={cn(
"mt-4 inline-flex w-full items-center justify-center gap-2 rounded-lg px-4 py-2.5 text-sm font-semibold transition-all duration-200 hover:-translate-y-0.5 hover:brightness-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 dark:focus-visible:ring-offset-dark-2",
isCopied
? "bg-gradient-to-br from-green to-green-dark text-white shadow-[0_6px_18px_-4px_rgba(34,173,92,0.55)] focus-visible:ring-green/40"
: "bg-gradient-to-br from-primary to-[#4C46C9] text-white shadow-[0_6px_18px_-4px_rgba(87,80,241,0.55)] focus-visible:ring-primary/40",
)}
data-cy="reset-password-copy-button"
>
<IsVisible condition={isCopied}>
<CheckIcon className="h-4 w-4" />
<span>Copied</span>
</IsVisible>
<IsVisible condition={!isCopied}>
<CopyIcon className="h-4 w-4" />
<span>Copy to clipboard</span>
</IsVisible>
</button>
</div>
</IsVisible>
</div>
);
};
export default ResetPasswordModalContent;
@@ -0,0 +1,47 @@
"use client";
import { useCallback, useEffect, useState } from "react";
import { toast } from "react-toastify";
const COPY_FEEDBACK_TIMEOUT_MS = 2000;
export const useResetPasswordModalPresenter = ({
isOpen,
password,
}: {
isOpen: boolean;
password: string | null;
}) => {
const [isCopied, setIsCopied] = useState(false);
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
useEffect(() => {
if (!isOpen) {
setIsCopied(false);
setIsPasswordVisible(false);
}
}, [isOpen]);
const togglePasswordVisibility = useCallback(() => {
setIsPasswordVisible((prev) => !prev);
}, []);
const copyPasswordToClipboard = useCallback(async () => {
if (!password) return;
try {
await navigator.clipboard.writeText(password);
setIsCopied(true);
toast.success("Password copied to clipboard");
setTimeout(() => setIsCopied(false), COPY_FEEDBACK_TIMEOUT_MS);
} catch {
toast.error("Failed to copy password");
}
}, [password]);
return {
isCopied,
isPasswordVisible,
togglePasswordVisibility,
copyPasswordToClipboard,
};
};