Merge pull request #17 from Myzel394/add-api

Add api
This commit is contained in:
Myzel394 2023-03-11 13:02:23 +01:00 committed by GitHub
commit 899208986f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 816 additions and 16 deletions

View File

@ -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",

View File

@ -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"
}
}
}

View File

@ -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"
}
}
}

View File

@ -2,6 +2,7 @@
"title": "Settings",
"actions": {
"enable2fa": "Two-Factor-Authentication",
"aliasPreferences": "Alias Preferences"
"aliasPreferences": "Alias Preferences",
"apiKeys": "Manage API Keys"
}
}

View File

@ -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?",

View File

@ -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: <Settings2FARoute />,
},
{
path: "/settings/api-keys",
loader: getServerSettings,
element: <SettingsAPIKeysRoute />,
},
{
path: "/reports",
loader: getServerSettings,
@ -162,9 +170,11 @@ export default function App(): ReactElement {
<QueryClientProvider client={queryClient}>
<ThemeProvider theme={THEME_THEME_MAP[theme]}>
<SnackbarProvider>
<CssBaseline />
<RouterProvider router={router} />
<I18nHandler />
<LocalizationProvider dateAdapter={AdapterDateFns}>
<CssBaseline />
<RouterProvider router={router} />
<I18nHandler />
</LocalizationProvider>
</SnackbarProvider>
</ThemeProvider>
</QueryClientProvider>

View File

@ -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<APIKey & {key: string}> {
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}
}

View File

@ -0,0 +1,10 @@
import {client} from "~/constants/axios-client"
import {SimpleDetailResponse} from "~/server-types"
export default async function deleteAPIKey(id: string): Promise<SimpleDetailResponse> {
const {data} = await client.delete(`${import.meta.env.VITE_SERVER_BASE_URL}/v1/api-key/${id}`, {
withCredentials: true,
})
return data
}

22
src/apis/get-api-keys.ts Normal file
View File

@ -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<APIKey>
> {
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),
}
}

View File

@ -0,0 +1,8 @@
import {APIKey} from "~/server-types"
export default function parseAPIKey(key: APIKey): APIKey {
return {
...key,
expiresAt: new Date(key.expiresAt),
}
}

View File

@ -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"

View File

@ -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 (
<>
<Button
variant="outlined"
color="error"
size="small"
startIcon={<MdDelete />}
onClick={() => setShowDeleteDialog(true)}
>
{label}
</Button>
{render ? (
render(() => setShowDeleteDialog(true))
) : (
<Button
variant="outlined"
color="error"
size="small"
startIcon={<MdDelete />}
onClick={() => setShowDeleteDialog(true)}
>
{label}
</Button>
)}
<Dialog open={showDeleteDialog} onClose={() => setShowDeleteDialog(false)}>
<DialogTitle>{label}</DialogTitle>
<DialogContent>

View File

@ -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",
]

View File

@ -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 (
<>
<Alert severity="success">
<ListItem>
<ListItemText>{privateKey}</ListItemText>
<ListItemSecondaryAction>
<IconButton edge="end" onClick={() => copy(privateKey)}>
<MdContentCopy />
</IconButton>
</ListItemSecondaryAction>
</ListItem>
</Alert>
<SuccessSnack
key={value}
message={value && t("messages.apiKey.keyCopied", {ns: "common"})}
/>
<ErrorSnack message={error && t("messages.errors.copyFailed", {ns: "common"})} />
</>
)
}
return (
<ListItem>
<>
<ListItemText primary={apiKey.label} secondary={apiKey.expiresAt.toString()} />
<ListItemSecondaryAction>
<DeleteButton
onDelete={() => 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 => (
<IconButton edge="end" onClick={onDelete}>
<MdDelete />
</IconButton>
)}
</DeleteButton>
</ListItemSecondaryAction>
</>
</ListItem>
)
}

View File

@ -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<string, ReactElement> = {
profile: <CgProfile />,
alias: <FaMask />,
report: <MdTextSnippet />,
preferences: <GoSettings />,
}
const API_KEY_SCOPE_TYPE_ICON_MAP: Record<string, ReactElement> = {
read: <TiEye />,
create: <MdAdd />,
update: <MdEdit />,
delete: <MdDelete />,
}
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<APIKeyScope[]>()
.of(yup.string())
.required()
.label(t("create.form.scopes.label")),
})
const {mutateAsync} = useMutation<APIKey & {key: string}, AxiosError, CreateAPIKeyData>(
data => createAPIKey(data),
{
onSuccess: async key => {
onClose()
showSuccess(t("create.success"))
onCreated(key)
},
},
)
const formik = useFormik<CreateAPIKeyData & {detail: string}>({
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 (
<Dialog open={open} onClose={onClose}>
<DialogTitle>{t("create.label")}</DialogTitle>
<form onSubmit={formik.handleSubmit}>
<DialogContent>
<DialogContentText>{t("create.description")}</DialogContentText>
<Grid container spacing={4} mt={1}>
<Grid item xs={12}>
<TextField
fullWidth
name="label"
id="label"
label={t("create.form.label.label")}
disabled={formik.isSubmitting}
error={formik.touched.label && Boolean(formik.errors.label)}
helperText={formik.touched.label && formik.errors.label}
value={formik.values.label}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<BiText />
</InputAdornment>
),
}}
/>
</Grid>
<Grid item xs={12}>
<FormControl
fullWidth
error={formik.touched.scopes && Boolean(formik.errors.scopes)}
>
<InputLabel htmlFor="scopes" id="scopes-label">
{t("create.form.scopes.label")}
</InputLabel>
<Select<string[]>
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[]) => (
<Box sx={{display: "flex", flexWrap: "wrap", gap: 0.5}}>
{selected.map(value => (
<Chip
key={value}
label={t(
`values.scopes.${value.replace(":", "_")}`,
{
ns: "common",
},
)}
/>
))}
</Box>
)}
>
{API_KEY_SCOPES.map(scope => (
<MenuItem key={scope} value={scope}>
<ListItem>
<ListItemIcon>
<Badge
badgeContent={
API_KEY_SCOPE_TYPE_ICON_MAP[
scope
.replace(":", "_")
.split("_")[0]
]
}
>
{
API_KEY_SCOPE_ICON_MAP[
scope
.replace(":", "_")
.split("_")[1]
]
}
</Badge>
</ListItemIcon>
<ListItemText>
{t(`values.scopes.${scope.replace(":", "_")}`, {
ns: "common",
})}
</ListItemText>
</ListItem>
</MenuItem>
))}
</Select>
<FormHelperText
error={Boolean(formik.touched.scopes && formik.errors.scopes)}
>
{formik.touched.scopes && formik.errors.scopes}
</FormHelperText>
</FormControl>
</Grid>
<Grid item xs={12}>
<Grid container spacing={1}>
<Grid item xs={12}>
<DatePicker
value={formik.values.expiresAt}
disabled={formik.isSubmitting}
label={t("create.form.expiresAt.label")}
slotProps={{
textField: {
fullWidth: true,
name: "expiresAt",
error:
formik.touched.expiresAt &&
Boolean(formik.errors.expiresAt),
helperText:
formik.touched.expiresAt &&
formik.errors.expiresAt,
onBlur: formik.handleBlur,
},
}}
minDate={new Date()}
maxDate={addDays(new Date(), serverSettings.apiKeyMaxDays)}
onChange={value => formik.setFieldValue("expiresAt", value)}
/>
<FormHelperText
error={
formik.touched.expiresAt &&
Boolean(formik.errors.expiresAt)
}
>
{formik.touched.expiresAt && formik.errors.expiresAt}
</FormHelperText>
</Grid>
<Grid item>
<Grid container spacing={1} direction="row">
{PRESET_DAYS.filter(
days => days <= serverSettings.apiKeyMaxDays,
).map(days => (
<Grid item key={days}>
<Chip
label={t(
`create.form.expiresAt.values.${days}days`,
)}
variant={
diffInDays(
formik.values.expiresAt!,
normalizeTime(new Date()),
) >= days
? "filled"
: "outlined"
}
onClick={() =>
formik.setFieldValue(
"expiresAt",
addDays(
normalizeTime(new Date()),
days,
),
)
}
/>
</Grid>
))}
</Grid>
</Grid>
</Grid>
</Grid>
{formik.errors.detail && (
<Grid item xs={12}>
<Alert severity="error">{formik.errors.detail}</Alert>
</Grid>
)}
</Grid>
</DialogContent>
<DialogActions>
<Button startIcon={<MdCancel />} onClick={onClose}>
{t("general.cancelLabel", {ns: "common"})}
</Button>
<Button type="submit" variant="contained">
{t("create.continueActionLabel")}
</Button>
</DialogActions>
</form>
</Dialog>
)
}

View File

@ -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 (
<Container maxWidth="xs">
<Grid
container
spacing={4}
direction="column"
alignItems="center"
maxWidth="80%"
alignSelf="center"
marginX="auto"
>
<Grid item>
<Typography variant="h6" component="h2">
{t("emptyState.title")}
</Typography>
</Grid>
<Grid item>
<MdVpnKey size={40} />
</Grid>
<Grid item>
<Typography variant="body1">{t("emptyState.description")}</Typography>
</Grid>
</Grid>
</Container>
)
}

View File

@ -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<PaginationResult<APIKey>, AxiosError>(queryKey, () => getAPIKeys())
const [createdAPIKey, setCreatedAPIKey] = useState<(APIKey & {key: string}) | null>(null)
const [createNew, setCreateNew] = useState<boolean>(false)
return (
<>
<SimplePage
title={t("title")}
actions={
<Button
variant="contained"
color="primary"
onClick={() => setCreateNew(true)}
startIcon={<MdAdd />}
>
{t("create.label")}
</Button>
}
>
<QueryResult<PaginationResult<APIKey>, AxiosError> query={query}>
{({items: apiKeys}) =>
apiKeys.length > 0 ? (
<List>
{apiKeys.map(apiKey => (
<APIKeyListItem
apiKey={apiKey}
key={apiKey.id}
queryKey={queryKey}
privateKey={
apiKey.id === createdAPIKey?.id
? createdAPIKey.key
: undefined
}
/>
))}
</List>
) : (
<EmptyStateScreen />
)
}
</QueryResult>
</SimplePage>
<CreateNewAPIKeyDialog
key={createNew.toString()}
open={createNew}
onClose={() => setCreateNew(false)}
onCreated={key => {
query.refetch()
setCreatedAPIKey(key)
}}
/>
</>
)
}

View File

@ -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 {
</ListItemIcon>
<ListItemText primary={t("actions.enable2fa")} />
</ListItemButton>
<ListItemButton component={Link} to="/settings/api-keys">
<ListItemIcon>
<MdVpnKey />
</ListItemIcon>
<ListItemText primary={t("actions.apiKeys")} />
</ListItemButton>
</List>
</SimplePageBuilder.Page>
)

View File

@ -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

View File

@ -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"