diff --git a/package.json b/package.json index 45423c9..02a2912 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@emotion/styled": "^11.10.4", "@mui/lab": "^5.0.0-alpha.103", "@mui/material": "^5.10.9", + "@mui/x-date-pickers": "^6.0.1", "@originjs/vite-plugin-commonjs": "^1.0.3", "@tanstack/react-query": "^4.12.0", "axios": "^1.1.2", diff --git a/public/locales/en-US/common.json b/public/locales/en-US/common.json index ce207da..74f89d5 100644 --- a/public/locales/en-US/common.json +++ b/public/locales/en-US/common.json @@ -59,6 +59,10 @@ }, "report": { "deleted": "Report has been deleted!" + }, + "apiKey": { + "keyCopied": "API key has been copied to your clipboard!", + "deleted": "API key has been deleted!" } }, "general": { @@ -89,5 +93,22 @@ "signup": "Sign up", "login": "Log in", "logout": "Log out" + }, + "values": { + "scopes": { + "basic_profile": "Basic Profile", + "full_profile": "Full Profile", + + "read_preferences": "Read Preferences", + "update_preferences": "Update Preferences", + + "read_alias": "Read Aliases", + "create_alias": "Create Aliases", + "update_alias": "Update Aliases", + "delete_alias": "Delete Aliases", + + "read_report": "Read Reports", + "delete_report": "Delete Reports" + } } } diff --git a/public/locales/en-US/settings-api-keys.json b/public/locales/en-US/settings-api-keys.json new file mode 100644 index 0000000..e909f35 --- /dev/null +++ b/public/locales/en-US/settings-api-keys.json @@ -0,0 +1,42 @@ +{ + "title": "Manage your API Keys", + "create": { + "label": "Create a new API Key", + "description": "Define a label and the scopes you want to grant to this API Key.", + "continueActionLabel": "Create API Key", + "success": "Your API Key has been created. Copy it now, you won't be able to see it again.", + "form": { + "label": { + "label": "Label" + }, + "expiresAt": { + "label": "Expiration date", + "errors": { + "tooFarInFuture": "This date is too far in the future.", + "inPast": "Expiration date must be in the future." + }, + "values": { + "1days": "1 Day", + "7days": "1 Week", + "30days": "1 Month", + "180days": "1/2 Year", + "360days": "1 Year" + } + }, + "scopes": { + "label": "Scopes" + } + } + }, + "emptyState": { + "title": "Welcome to your API Keys", + "description": "Create an API Key to get started with the API." + }, + "actions": { + "delete": { + "label": "Delete API Key", + "description": "Are you sure you want to delete this API Key? Your existing integrations using this API Key will stop working. You can create a new API Key to replace it.", + "continueActionLabel": "Delete API Key" + } + } +} diff --git a/public/locales/en-US/settings.json b/public/locales/en-US/settings.json index 7ab1855..756d4aa 100644 --- a/public/locales/en-US/settings.json +++ b/public/locales/en-US/settings.json @@ -2,6 +2,7 @@ "title": "Settings", "actions": { "enable2fa": "Two-Factor-Authentication", - "aliasPreferences": "Alias Preferences" + "aliasPreferences": "Alias Preferences", + "apiKeys": "Manage API Keys" } } diff --git a/public/locales/en-US/signup.json b/public/locales/en-US/signup.json index dc1b562..e7486e7 100644 --- a/public/locales/en-US/signup.json +++ b/public/locales/en-US/signup.json @@ -7,7 +7,7 @@ "mailVerification": { "title": "You got mail!", "description": "We sent you an email with a link to confirm your email address. Please check your inbox and click on the link to continue.", - "maskAsNotSpam": "If you can't find the email, please check your spam folder. If the email lands in your spam folder, please mark it as \"Not Spam\" to receive all emails in the future", + "markAsNotSpam": "If you can't find the email, please check your spam folder. If the email lands in your spam folder, please mark it as \"Not Spam\" to receive all emails in the future", "editEmail": { "title": "Edit email address?", "description": "Would you like to return to the previous step and edit your email address?", diff --git a/src/App.tsx b/src/App.tsx index c54ecea..48a0768 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,6 +7,8 @@ import React, {ReactElement} from "react" import {queryClient} from "~/constants/react-query" import {getServerSettings} from "~/apis" import {darkTheme, lightTheme} from "~/constants/themes" +import {LocalizationProvider} from "@mui/x-date-pickers" +import {AdapterDateFns} from "@mui/x-date-pickers/AdapterDateFns" import AdminRoute from "~/routes/AdminRoute" import AliasDetailRoute from "~/routes/AliasDetailRoute" import AliasesRoute from "~/routes/AliasesRoute" @@ -28,6 +30,7 @@ import ReservedAliasDetailRoute from "~/routes/ReservedAliasDetailRoute" import ReservedAliasesRoute from "~/routes/ReservedAliasesRoute" import RootRoute from "~/routes/Root" import Settings2FARoute from "~/routes/Settings2FARoute" +import SettingsAPIKeysRoute from "~/routes/SettingsAPIKeysRoute" import SettingsAliasPreferencesRoute from "~/routes/SettingsAliasPreferencesRoute" import SettingsRoute from "~/routes/SettingsRoute" import SignupRoute from "~/routes/SignupRoute" @@ -105,6 +108,11 @@ const router = createBrowserRouter([ path: "/settings/2fa", element: , }, + { + path: "/settings/api-keys", + loader: getServerSettings, + element: , + }, { path: "/reports", loader: getServerSettings, @@ -162,9 +170,11 @@ export default function App(): ReactElement { - - - + + + + + diff --git a/src/apis/create-api-key.ts b/src/apis/create-api-key.ts new file mode 100644 index 0000000..ac7582c --- /dev/null +++ b/src/apis/create-api-key.ts @@ -0,0 +1,35 @@ +import {APIKey} from "~/server-types" +import {client} from "~/constants/axios-client" +import formatISO from "date-fns/formatISO" +import parseAPIKey from "~/apis/helpers/parse-api-key" + +export interface CreateAPIKeyData { + label: string + scopes: APIKey["scopes"] + + expiresAt?: Date +} + +export default async function createAPIKey({ + label, + scopes, + expiresAt, +}: CreateAPIKeyData): Promise { + const {data} = await client.post( + `${import.meta.env.VITE_SERVER_BASE_URL}/v1/api-key`, + { + label, + scopes, + expiresAt: expiresAt + ? formatISO(expiresAt, { + representation: "date", + }) + : undefined, + }, + { + withCredentials: true, + }, + ) + + return parseAPIKey(data) as APIKey & {key: string} +} diff --git a/src/apis/delete-api-key.ts b/src/apis/delete-api-key.ts new file mode 100644 index 0000000..9280592 --- /dev/null +++ b/src/apis/delete-api-key.ts @@ -0,0 +1,10 @@ +import {client} from "~/constants/axios-client" +import {SimpleDetailResponse} from "~/server-types" + +export default async function deleteAPIKey(id: string): Promise { + const {data} = await client.delete(`${import.meta.env.VITE_SERVER_BASE_URL}/v1/api-key/${id}`, { + withCredentials: true, + }) + + return data +} diff --git a/src/apis/get-api-keys.ts b/src/apis/get-api-keys.ts new file mode 100644 index 0000000..f927dd3 --- /dev/null +++ b/src/apis/get-api-keys.ts @@ -0,0 +1,22 @@ +import {APIKey, GetPageData, PaginationResult} from "~/server-types" +import {client} from "~/constants/axios-client" +import parseAPIKey from "~/apis/helpers/parse-api-key" + +export interface GetAPIKeysData extends GetPageData {} + +export default async function getAPIKeys({size, page}: GetAPIKeysData = {}): Promise< + PaginationResult +> { + const {data} = await client.get(`${import.meta.env.VITE_SERVER_BASE_URL}/v1/api-key/`, { + withCredentials: true, + params: { + size, + page, + }, + }) + + return { + ...data, + items: data.items.map(parseAPIKey), + } +} diff --git a/src/apis/helpers/parse-api-key.ts b/src/apis/helpers/parse-api-key.ts new file mode 100644 index 0000000..d62c51a --- /dev/null +++ b/src/apis/helpers/parse-api-key.ts @@ -0,0 +1,8 @@ +import {APIKey} from "~/server-types" + +export default function parseAPIKey(key: APIKey): APIKey { + return { + ...key, + expiresAt: new Date(key.expiresAt), + } +} diff --git a/src/apis/index.ts b/src/apis/index.ts index 7340c69..ce3aceb 100644 --- a/src/apis/index.ts +++ b/src/apis/index.ts @@ -68,3 +68,9 @@ export * from "./delete-2fa" export {default as delete2FA} from "./delete-2fa" export * from "./verify-otp" export {default as verifyOTP} from "./verify-otp" +export * from "./create-api-key" +export {default as createAPIKey} from "./create-api-key" +export * from "./get-api-keys" +export {default as getAPIKeys} from "./get-api-keys" +export * from "./delete-api-key" +export {default as deleteAPIKey} from "./delete-api-key" diff --git a/src/components/widgets/DeleteAPIButton.tsx b/src/components/widgets/DeleteAPIButton.tsx index 5a8e30b..e6da96c 100644 --- a/src/components/widgets/DeleteAPIButton.tsx +++ b/src/components/widgets/DeleteAPIButton.tsx @@ -25,6 +25,8 @@ export interface DeleteAPIButtonProps { description?: string successMessage?: string navigateTo?: string + children?: (onDelete: () => void) => ReactElement + onDone?: () => void } export default function DeleteAPIButton({ @@ -33,7 +35,9 @@ export default function DeleteAPIButton({ label, continueLabel, description, - navigateTo = "/aliases", + navigateTo, + onDone, + children: render, }: DeleteAPIButtonProps): ReactElement { const {t} = useTranslation("common") const {showError, showSuccess} = useErrorSuccessSnacks() @@ -43,7 +47,12 @@ export default function DeleteAPIButton({ onError: showError, onSuccess: () => { showSuccess(successMessage || t("messages.deletedObject")) - navigate(navigateTo) + + if (navigateTo) { + navigate(navigateTo) + } else if (onDone) { + onDone() + } }, }) @@ -51,15 +60,19 @@ export default function DeleteAPIButton({ return ( <> - + {render ? ( + render(() => setShowDeleteDialog(true)) + ) : ( + + )} setShowDeleteDialog(false)}> {label} diff --git a/src/constants/values.ts b/src/constants/values.ts index 430804c..453e7a5 100644 --- a/src/constants/values.ts +++ b/src/constants/values.ts @@ -16,3 +16,18 @@ export const DEFAULT_ALIAS_NOTE: AliasNote = { export const ERROR_SNACKBAR_SHOW_DURATION = 5000 export const SUCCESS_SNACKBAR_SHOW_DURATION = 2000 export const AUTHENTICATION_PATHS = ["/auth/login", "/auth/signup", "/auth/complete-account"] +export const API_KEY_SCOPES = [ + "basic_profile", + "full_profile", + + "read:preferences", + "update:preferences", + + "read:alias", + "create:alias", + "update:alias", + "delete:alias", + + "read:report", + "delete:report", +] diff --git a/src/route-widgets/SettingsAPIKeysRoute/APIKeyListItem.tsx b/src/route-widgets/SettingsAPIKeysRoute/APIKeyListItem.tsx new file mode 100644 index 0000000..760346a --- /dev/null +++ b/src/route-widgets/SettingsAPIKeysRoute/APIKeyListItem.tsx @@ -0,0 +1,75 @@ +import {ReactElement} from "react" +import {APIKey} from "~/server-types" +import {Alert, IconButton, ListItem, ListItemSecondaryAction, ListItemText} from "@mui/material" +import {useTranslation} from "react-i18next" +import {MdContentCopy, MdDelete} from "react-icons/md" +import {useCopyToClipboard} from "react-use" +import {DeleteButton, ErrorSnack, SuccessSnack} from "~/components" +import {deleteAPIKey} from "~/apis" +import {queryClient} from "~/constants/react-query" + +export interface APIKeyListItemProps { + apiKey: APIKey + queryKey: readonly string[] + privateKey?: string +} + +export default function APIKeyListItem({ + queryKey, + apiKey, + privateKey, +}: APIKeyListItemProps): ReactElement { + const {t} = useTranslation(["settings-api-keys", "common"]) + + const [{value, error}, copy] = useCopyToClipboard() + + if (privateKey) { + return ( + <> + + + {privateKey} + + copy(privateKey)}> + + + + + + + + + ) + } + + return ( + + <> + + + deleteAPIKey(apiKey.id)} + onDone={() => + queryClient.invalidateQueries({ + queryKey, + }) + } + label={t("actions.delete.label")} + description={t("actions.delete.description")} + continueLabel={t("actions.delete.continueActionLabel")} + successMessage={t("messages.apiKey.deleted", {ns: "common"})} + > + {onDelete => ( + + + + )} + + + + + ) +} diff --git a/src/route-widgets/SettingsAPIKeysRoute/CreateNewAPIKeyDialog.tsx b/src/route-widgets/SettingsAPIKeysRoute/CreateNewAPIKeyDialog.tsx new file mode 100644 index 0000000..cdabcb5 --- /dev/null +++ b/src/route-widgets/SettingsAPIKeysRoute/CreateNewAPIKeyDialog.tsx @@ -0,0 +1,311 @@ +import * as yup from "yup" +import {ReactElement} from "react" +import { + Alert, + Badge, + Box, + Button, + Chip, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + FormControl, + FormHelperText, + Grid, + InputAdornment, + InputLabel, + ListItem, + ListItemIcon, + ListItemText, + MenuItem, + Select, + TextField, +} from "@mui/material" +import {useTranslation} from "react-i18next" +import {useFormik} from "formik" +import {CreateAPIKeyData, createAPIKey} from "~/apis" +import {APIKey, APIKeyScope, ServerSettings} from "~/server-types" +import {BiText} from "react-icons/bi" +import {CgProfile} from "react-icons/cg" +import {DatePicker} from "@mui/x-date-pickers" +import {useLoaderData} from "react-router-dom" +import {API_KEY_SCOPES} from "~/constants/values" +import {FaMask} from "react-icons/fa" +import {MdAdd, MdCancel, MdDelete, MdEdit, MdTextSnippet} from "react-icons/md" +import {GoSettings} from "react-icons/go" +import {TiEye} from "react-icons/ti" +import {useMutation} from "@tanstack/react-query" +import {AxiosError} from "axios" +import {parseFastAPIError} from "~/utils" +import {useErrorSuccessSnacks} from "~/hooks" +import addDays from "date-fns/addDays" +import diffInDays from "date-fns/differenceInDays" +import set from "date-fns/set" + +export interface CreateNewAPIKeyDialogProps { + open: boolean + onClose: () => void + onCreated: (key: APIKey & {key: string}) => void +} + +const PRESET_DAYS: number[] = [1, 7, 30, 180, 360] + +const API_KEY_SCOPE_ICON_MAP: Record = { + profile: , + alias: , + report: , + preferences: , +} + +const API_KEY_SCOPE_TYPE_ICON_MAP: Record = { + read: , + create: , + update: , + delete: , +} + +const normalizeTime = (date: Date) => + set(date, { + hours: 0, + minutes: 0, + seconds: 0, + milliseconds: 0, + }) + +export default function CreateNewAPIKeyDialog({ + open, + onClose, + onCreated, +}: CreateNewAPIKeyDialogProps): ReactElement { + const {t} = useTranslation(["settings-api-keys", "common"]) + const serverSettings = useLoaderData() as ServerSettings + const {showSuccess} = useErrorSuccessSnacks() + + const scheme = yup.object().shape({ + label: yup.string().required().label(t("create.form.label.label")), + expiresAt: yup.date().required().label(t("create.form.expiresAt.label")), + scopes: yup + .array() + .of(yup.string()) + .required() + .label(t("create.form.scopes.label")), + }) + + const {mutateAsync} = useMutation( + data => createAPIKey(data), + { + onSuccess: async key => { + onClose() + + showSuccess(t("create.success")) + + onCreated(key) + }, + }, + ) + const formik = useFormik({ + validationSchema: scheme, + initialValues: { + label: "", + expiresAt: addDays(new Date(), 30), + scopes: [], + detail: "", + }, + onSubmit: async (values, {setErrors}) => { + try { + await mutateAsync(values) + } catch (error) { + setErrors(parseFastAPIError(error as AxiosError)) + } + }, + }) + + return ( + + {t("create.label")} +
+ + {t("create.description")} + + + + + + ), + }} + /> + + + + + {t("create.form.scopes.label")} + + + multiple + fullWidth + name="scopes" + id="scopes" + label={t("create.form.scopes.label")} + disabled={formik.isSubmitting} + error={formik.touched.scopes && Boolean(formik.errors.scopes)} + value={formik.values.scopes} + onChange={formik.handleChange} + onBlur={formik.handleBlur} + renderValue={(selected: string[]) => ( + + {selected.map(value => ( + + ))} + + )} + > + {API_KEY_SCOPES.map(scope => ( + + + + + { + API_KEY_SCOPE_ICON_MAP[ + scope + .replace(":", "_") + .split("_")[1] + ] + } + + + + {t(`values.scopes.${scope.replace(":", "_")}`, { + ns: "common", + })} + + + + ))} + + + {formik.touched.scopes && formik.errors.scopes} + + + + + + + formik.setFieldValue("expiresAt", value)} + /> + + {formik.touched.expiresAt && formik.errors.expiresAt} + + + + + {PRESET_DAYS.filter( + days => days <= serverSettings.apiKeyMaxDays, + ).map(days => ( + + = days + ? "filled" + : "outlined" + } + onClick={() => + formik.setFieldValue( + "expiresAt", + addDays( + normalizeTime(new Date()), + days, + ), + ) + } + /> + + ))} + + + + + {formik.errors.detail && ( + + {formik.errors.detail} + + )} + + + + + + +
+
+ ) +} diff --git a/src/route-widgets/SettingsAPIKeysRoute/EmptyStateScreen.tsx b/src/route-widgets/SettingsAPIKeysRoute/EmptyStateScreen.tsx new file mode 100644 index 0000000..196144b --- /dev/null +++ b/src/route-widgets/SettingsAPIKeysRoute/EmptyStateScreen.tsx @@ -0,0 +1,35 @@ +import {ReactElement} from "react" +import {useTranslation} from "react-i18next" + +import {Container, Grid, Typography} from "@mui/material" +import {MdVpnKey} from "react-icons/md" + +export default function EmptyStateScreen(): ReactElement { + const {t} = useTranslation("settings-api-keys") + + return ( + + + + + {t("emptyState.title")} + + + + + + + {t("emptyState.description")} + + + + ) +} diff --git a/src/routes/SettingsAPIKeysRoute.tsx b/src/routes/SettingsAPIKeysRoute.tsx new file mode 100644 index 0000000..922f4b2 --- /dev/null +++ b/src/routes/SettingsAPIKeysRoute.tsx @@ -0,0 +1,71 @@ +import {ReactElement, useState} from "react" +import {useTranslation} from "react-i18next" +import {useQuery} from "@tanstack/react-query" +import {APIKey, PaginationResult} from "~/server-types" +import {AxiosError} from "axios" +import {getAPIKeys} from "~/apis" +import {QueryResult, SimplePage} from "~/components" +import {Button, List} from "@mui/material" +import {MdAdd} from "react-icons/md" +import APIKeyListItem from "~/route-widgets/SettingsAPIKeysRoute/APIKeyListItem" +import CreateNewAPIKeyDialog from "../route-widgets/SettingsAPIKeysRoute/CreateNewAPIKeyDialog" +import EmptyStateScreen from "~/route-widgets/SettingsAPIKeysRoute/EmptyStateScreen" + +export default function SettingsAPIKeysRoute(): ReactElement { + const {t} = useTranslation("settings-api-keys") + const queryKey = ["get_api_keys"] + const query = useQuery, AxiosError>(queryKey, () => getAPIKeys()) + + const [createdAPIKey, setCreatedAPIKey] = useState<(APIKey & {key: string}) | null>(null) + const [createNew, setCreateNew] = useState(false) + + return ( + <> + setCreateNew(true)} + startIcon={} + > + {t("create.label")} + + } + > + , AxiosError> query={query}> + {({items: apiKeys}) => + apiKeys.length > 0 ? ( + + {apiKeys.map(apiKey => ( + + ))} + + ) : ( + + ) + } + + + setCreateNew(false)} + onCreated={key => { + query.refetch() + setCreatedAPIKey(key) + }} + /> + + ) +} diff --git a/src/routes/SettingsRoute.tsx b/src/routes/SettingsRoute.tsx index bb0b06e..a103fe6 100644 --- a/src/routes/SettingsRoute.tsx +++ b/src/routes/SettingsRoute.tsx @@ -1,6 +1,7 @@ import {useTranslation} from "react-i18next" import {GoSettings} from "react-icons/go" import {BsShieldLockFill} from "react-icons/bs" +import {MdVpnKey} from "react-icons/md" import {Link} from "react-router-dom" import React, {ReactElement} from "react" @@ -26,6 +27,12 @@ export default function SettingsRoute(): ReactElement { + + + + + + ) diff --git a/src/server-types.ts b/src/server-types.ts index 62642c7..9f8535e 100644 --- a/src/server-types.ts +++ b/src/server-types.ts @@ -84,6 +84,7 @@ export interface ServerSettings { instanceSalt: string publicKey: string allowAliasDeletion: boolean + apiKeyMaxDays: number } export interface Alias { @@ -141,6 +142,25 @@ export interface AliasList { type: AliasType } +export type APIKeyScope = + | "full_profile" + | "basic_profile" + | "read:preferences" + | "update:preferences" + | "read:alias" + | "create:alias" + | "update:alias" + | "delete:alias" + | "read:report" + | "delete:report" + +export interface APIKey { + id: string + label: string + expiresAt: Date + scopes: APIKeyScope[] +} + export interface Report { id: string encryptedContent: string diff --git a/yarn.lock b/yarn.lock index 4d7337a..9763a1a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -464,6 +464,13 @@ dependencies: regenerator-runtime "^0.13.10" +"@babel/runtime@^7.20.13", "@babel/runtime@^7.21.0": + version "7.21.0" + resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz#5b55c9d394e5fcf304909a8b00c07dc217b56673" + integrity sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw== + dependencies: + regenerator-runtime "^0.13.11" + "@babel/template@^7.18.10": version "7.18.10" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.10.tgz#6f9134835970d1dbf0835c0d100c9f38de0c5e71" @@ -562,6 +569,60 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== +"@date-io/core@^2.16.0": + version "2.16.0" + resolved "https://registry.npmjs.org/@date-io/core/-/core-2.16.0.tgz#7871bfc1d9bca9aa35ad444a239505589d0f22f6" + integrity sha512-DYmSzkr+jToahwWrsiRA2/pzMEtz9Bq1euJwoOuYwuwIYXnZFtHajY2E6a1VNVDc9jP8YUXK1BvnZH9mmT19Zg== + +"@date-io/date-fns-jalali@^2.16.0": + version "2.16.0" + resolved "https://registry.npmjs.org/@date-io/date-fns-jalali/-/date-fns-jalali-2.16.0.tgz#0e2916c287e00d708a6f9f6d6e6c44ec377994c5" + integrity sha512-MNVvGYwRiBydbvY7gvZM14W2kosIG29G1Ekw5qmYWOXkIIFngh6ZvV7/uVGDCW+gqlIeSz/XitZXA9n8RO0tJw== + dependencies: + "@date-io/core" "^2.16.0" + +"@date-io/date-fns@^2.16.0": + version "2.16.0" + resolved "https://registry.npmjs.org/@date-io/date-fns/-/date-fns-2.16.0.tgz#bd5e09b6ecb47ee55e593fc3a87e7b2caaa3da40" + integrity sha512-bfm5FJjucqlrnQcXDVU5RD+nlGmL3iWgkHTq3uAZWVIuBu6dDmGa3m8a6zo2VQQpu8ambq9H22UyUpn7590joA== + dependencies: + "@date-io/core" "^2.16.0" + +"@date-io/dayjs@^2.16.0": + version "2.16.0" + resolved "https://registry.npmjs.org/@date-io/dayjs/-/dayjs-2.16.0.tgz#0d2c254ad8db1306fdc4b8eda197cb53c9af89dc" + integrity sha512-y5qKyX2j/HG3zMvIxTobYZRGnd1FUW2olZLS0vTj7bEkBQkjd2RO7/FEwDY03Z1geVGlXKnzIATEVBVaGzV4Iw== + dependencies: + "@date-io/core" "^2.16.0" + +"@date-io/hijri@^2.16.1": + version "2.16.1" + resolved "https://registry.npmjs.org/@date-io/hijri/-/hijri-2.16.1.tgz#a5e7e5b875e0ac8719c235eaf51d4188f21ea193" + integrity sha512-6BxY0mtnqj5cBiXluRs3uWN0mSJwGw0AB2ZxqtEHvBFoiSYEojW51AETnfPIWpdvDsBn+WAC7QrfBvQZnoyIkQ== + dependencies: + "@date-io/moment" "^2.16.1" + +"@date-io/jalaali@^2.16.1": + version "2.16.1" + resolved "https://registry.npmjs.org/@date-io/jalaali/-/jalaali-2.16.1.tgz#c53323fc429f8fe6ab205d36c003d6de071f17e8" + integrity sha512-GLw87G/WJ1DNrQHW8p/LqkqAqTUSqBSRin0H1pRPwCccB5Fh7GT64sadjzEvjW56lPJ0aq2vp5yI2eIjZajfrw== + dependencies: + "@date-io/moment" "^2.16.1" + +"@date-io/luxon@^2.16.1": + version "2.16.1" + resolved "https://registry.npmjs.org/@date-io/luxon/-/luxon-2.16.1.tgz#b08786614cb58831c729a15807753011e4acb966" + integrity sha512-aeYp5K9PSHV28946pC+9UKUi/xMMYoaGelrpDibZSgHu2VWHXrr7zWLEr+pMPThSs5vt8Ei365PO+84pCm37WQ== + dependencies: + "@date-io/core" "^2.16.0" + +"@date-io/moment@^2.16.1": + version "2.16.1" + resolved "https://registry.npmjs.org/@date-io/moment/-/moment-2.16.1.tgz#ec6e0daa486871e0e6412036c6f806842a0eeed4" + integrity sha512-JkxldQxUqZBfZtsaCcCMkm/dmytdyq5pS1RxshCQ4fHhsvP5A7gSqPD22QbVXMcJydi3d3v1Y8BQdUKEuGACZQ== + dependencies: + "@date-io/core" "^2.16.0" + "@emotion/babel-plugin@^11.10.0": version "11.10.2" resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.10.2.tgz#879db80ba622b3f6076917a1e6f648b1c7d008c7" @@ -1077,6 +1138,37 @@ prop-types "^15.8.1" react-is "^18.2.0" +"@mui/utils@^5.11.7": + version "5.11.12" + resolved "https://registry.npmjs.org/@mui/utils/-/utils-5.11.12.tgz#627f491c0e7267398590af5e6cb14b306170d914" + integrity sha512-5vH9B/v8pzkpEPO2HvGM54ToXV6cFdAn8UrvdN8TMEEwpn/ycW0jLiyBcgUlPsQ+xha7hqXCPQYHaYFDIcwaiw== + dependencies: + "@babel/runtime" "^7.21.0" + "@types/prop-types" "^15.7.5" + "@types/react-is" "^16.7.1 || ^17.0.0" + prop-types "^15.8.1" + react-is "^18.2.0" + +"@mui/x-date-pickers@^6.0.1": + version "6.0.1" + resolved "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-6.0.1.tgz#855f71b0661602c105dfd75bd0d38cd8c40d59f5" + integrity sha512-eZ3uTWp2uA08h4IULoqI7rkP16ltzZH0kSdZdvLCvaa4ix1ZP5zGcvt8CdtxM+NFWjRS7fGWPOgOoScGw9lKLQ== + dependencies: + "@babel/runtime" "^7.20.13" + "@date-io/core" "^2.16.0" + "@date-io/date-fns" "^2.16.0" + "@date-io/date-fns-jalali" "^2.16.0" + "@date-io/dayjs" "^2.16.0" + "@date-io/hijri" "^2.16.1" + "@date-io/jalaali" "^2.16.1" + "@date-io/luxon" "^2.16.1" + "@date-io/moment" "^2.16.1" + "@mui/utils" "^5.11.7" + "@types/react-transition-group" "^4.4.5" + clsx "^1.2.1" + prop-types "^15.8.1" + react-transition-group "^4.4.5" + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -4774,6 +4866,11 @@ regenerator-runtime@^0.13.10, regenerator-runtime@^0.13.4: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.10.tgz#ed07b19616bcbec5da6274ebc75ae95634bfc2ee" integrity sha512-KepLsg4dU12hryUO7bp/axHAKvwGOCV0sGloQtpagJ12ai+ojVDqkeGSiRX1zlq+kjIMZ1t7gpze+26QqtdGqw== +regenerator-runtime@^0.13.11: + version "0.13.11" + resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" + integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== + regexp.prototype.flags@^1.3.0, regexp.prototype.flags@^1.4.1, regexp.prototype.flags@^1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac"