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
+51
View File
@@ -600,6 +600,57 @@ export function PencilSquareIcon(props: IconProps) {
);
}
export function KeyIcon(props: IconProps) {
return (
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M15.5 11.5C18.5376 11.5 21 9.03757 21 6C21 2.96243 18.5376 0.5 15.5 0.5C12.4624 0.5 10 2.96243 10 6C10 6.79363 10.168 7.5481 10.4705 8.22988L0.5 18.2003V23.5H5.79972L7.40195 21.8978L7.40123 19.7032L9.59639 19.7039L9.59707 17.5093L11.792 17.5093L15.2701 14.0295C15.9519 14.332 16.7064 14.5 17.5 14.5"
stroke="currentColor"
strokeWidth="1.6"
strokeLinecap="round"
strokeLinejoin="round"
/>
<circle cx="16.75" cy="5" r="1.25" fill="currentColor" />
</svg>
);
}
export function CopyIcon(props: IconProps) {
return (
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<rect
x="8"
y="8"
width="13"
height="13"
rx="2.5"
stroke="currentColor"
strokeWidth="1.6"
/>
<path
d="M16 8V5.5C16 4.39543 15.1046 3.5 14 3.5H5.5C4.39543 3.5 3.5 4.39543 3.5 5.5V14C3.5 15.1046 4.39543 16 5.5 16H8"
stroke="currentColor"
strokeWidth="1.6"
strokeLinecap="round"
/>
</svg>
);
}
export function UploadIcon(props: IconProps) {
return (
<svg
+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,
};
};
+6 -6
View File
@@ -13,42 +13,42 @@ const variantPalettes: Record<string, Palette> = {
from: "#F23030",
to: "#B00B0B",
shadow:
"0 10px 30px -10px rgba(242,48,48,0.55), inset 0 1px 0 rgba(255,255,255,0.18)",
"0 0 24px -4px rgba(242,48,48,0.55), inset 0 1px 0 rgba(255,255,255,0.18)",
border: "rgba(255,255,255,0.14)",
},
info: {
from: "#5750F1",
to: "#3B36B3",
shadow:
"0 10px 30px -10px rgba(87,80,241,0.55), inset 0 1px 0 rgba(255,255,255,0.2)",
"0 0 24px -4px rgba(87,80,241,0.55), inset 0 1px 0 rgba(255,255,255,0.2)",
border: "rgba(255,255,255,0.16)",
},
success: {
from: "#22AD5C",
to: "#147F3D",
shadow:
"0 10px 30px -10px rgba(34,173,92,0.55), inset 0 1px 0 rgba(255,255,255,0.2)",
"0 0 24px -4px rgba(34,173,92,0.55), inset 0 1px 0 rgba(255,255,255,0.2)",
border: "rgba(255,255,255,0.16)",
},
warning: {
from: "#FBB040",
to: "#D97706",
shadow:
"0 10px 30px -10px rgba(217,119,6,0.55), inset 0 1px 0 rgba(255,255,255,0.2)",
"0 0 24px -4px rgba(217,119,6,0.55), inset 0 1px 0 rgba(255,255,255,0.2)",
border: "rgba(255,255,255,0.16)",
},
dark: {
from: "#1F2937",
to: "#0B1220",
shadow:
"0 10px 30px -10px rgba(0,0,0,0.65), inset 0 1px 0 rgba(255,255,255,0.08)",
"0 0 24px -4px rgba(0,0,0,0.65), inset 0 1px 0 rgba(255,255,255,0.08)",
border: "rgba(255,255,255,0.08)",
},
light: {
from: "#FFFFFF",
to: "#F1F5F9",
shadow:
"0 10px 30px -10px rgba(0,0,0,0.18), inset 0 1px 0 rgba(255,255,255,0.6)",
"0 0 24px -4px rgba(0,0,0,0.18), inset 0 1px 0 rgba(255,255,255,0.6)",
border: "rgba(15,23,42,0.06)",
},
};
+19
View File
@@ -129,6 +129,25 @@ export const useUpdateCustomer = (id: string) => {
});
};
export const useResetCustomerPassword = ({
successCallback,
}: {
successCallback?: (password: string) => void;
} = {}) => {
const mutationKey = useMemo(
() => [API_ENDPOINTS.CUSTOMERS.READ_ALL, "reset-password"],
[],
);
return useMutation({
mutationKey,
mutationFn: customersService.resetCustomerPassword,
onSuccess: (response) => {
toast.success(response.message);
successCallback?.(response.data.password);
},
});
};
export const useDeleteCustomerQuery = (successCallback?: () => void) => {
const queryClient = useQueryClient();
const mutationKey = useMemo(
+19
View File
@@ -135,6 +135,25 @@ export const useGetUsersQuery = (params?: Record<string, any>) => {
queryFn: () => userService.adminsList(params),
});
};
export const useResetAdminPassword = ({
successCallback,
}: {
successCallback?: (password: string) => void;
} = {}) => {
const mutationKey = useMemo(
() => [API_ENDPOINTS.USER.ADMINS, "reset-password"],
[],
);
return useMutation({
mutationKey,
mutationFn: userService.resetAdminPassword,
onSuccess: (response) => {
toast.success(response.message);
successCallback?.(response.data.password);
},
});
};
export const useChangeAdminStatus = ({
id,
successCallback,
+2
View File
@@ -13,6 +13,7 @@ export const API_ENDPOINTS = {
ADMINS: "users",
ADMIN_DETAILS: (id: string) => `users/${id}`,
CHANGE_ADMIN_STATUS: (id: string) => `users/${id}/status`,
RESET_ADMIN_PASSWORD: (id: string) => `users/${id}/reset-password`,
},
FILES: {
UPLOAD: "files/upload",
@@ -55,6 +56,7 @@ export const API_ENDPOINTS = {
DELETE: (id: string) => `customers/${id}`,
DETAILS: (id: string) => `customers/${id}`,
ASSIGN_COMPANY_TO_CUSTOMER: (id: string) => `customers/${id}/companies`,
RESET_PASSWORD: (id: string) => `customers/${id}/reset-password`,
},
FEEDBACK: {
READ_ALL: "feedback",
+14 -1
View File
@@ -1,6 +1,6 @@
import api from "../axios";
import { API_ENDPOINTS } from "../endpoints";
import { ApiResponse } from "../types";
import { ApiResponse, TResetPasswordResponse } from "../types";
import {
CreateCustomerCredentials,
CustomerListResponseSchema,
@@ -57,6 +57,19 @@ export const customersService = {
const response = await api.get(API_ENDPOINTS.CUSTOMERS.DETAILS(id));
return response.data;
},
resetCustomerPassword: async (
id: string,
): Promise<ApiResponse<TResetPasswordResponse>> => {
try {
return (await api.post(API_ENDPOINTS.CUSTOMERS.RESET_PASSWORD(id))).data;
} catch (error) {
console.error(
"ERROR caught in Customers Service Reset Password:",
error,
);
throw error;
}
},
assignCompanyToCustomer: async ({
credentials,
id,
+11
View File
@@ -10,6 +10,7 @@ import {
LoginResponseSchema,
LogoutResponseSchema,
TChangeAdminStatusCredentials,
TResetPasswordResponse,
} from "../types";
export const userService = {
@@ -92,6 +93,16 @@ export const userService = {
throw error;
}
},
resetAdminPassword: async (
id: string,
): Promise<ApiResponse<TResetPasswordResponse>> => {
try {
return (await api.post(API_ENDPOINTS.USER.RESET_ADMIN_PASSWORD(id))).data;
} catch (error) {
console.error("ERROR caught in User Service Reset Admin Password", error);
throw error;
}
},
changeAdminStatus: async ({
id,
credentials,
+6
View File
@@ -69,6 +69,9 @@ export const LogoutResponse = z.object({
export const changeAdminStatusCredentialsSchema = z.object({
status: z.string(),
});
export const ResetPasswordResponseSchema = z.object({
password: z.string(),
});
export type IUser = z.infer<typeof UserSchema>;
export type ILogoutResponse = z.infer<typeof LogoutResponse>;
export type ILoginCredentials = z.infer<typeof LoginCredentials>;
@@ -77,6 +80,9 @@ export type IAdminListResponse = z.infer<typeof AdminListResponse>;
export type TChangeAdminStatusCredentials = z.infer<
typeof changeAdminStatusCredentialsSchema
>;
export type TResetPasswordResponse = z.infer<
typeof ResetPasswordResponseSchema
>;
export const LogoutResponseSchema = createApiResponseSchema(LogoutResponse);
export const LoginResponseSchema = createApiResponseSchema(LoginResponse);
export const AdminListResponseSchema =