mirror of
https://github.com/Myzel394/kleckrelay-website.git
synced 2025-06-18 23:45:26 +02:00
feat(api-key): Add create api key functionality
This commit is contained in:
parent
eeb6ba4892
commit
dace44f1d9
@ -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",
|
||||
|
@ -59,6 +59,9 @@
|
||||
},
|
||||
"report": {
|
||||
"deleted": "Report has been deleted!"
|
||||
},
|
||||
"apiKey": {
|
||||
"keyCopied": "API key has been copied to your clipboard!"
|
||||
}
|
||||
},
|
||||
"general": {
|
||||
@ -89,5 +92,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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,32 @@
|
||||
{
|
||||
"title": "Manage your API Keys",
|
||||
"create": {
|
||||
"label": "Create a new API Key"
|
||||
"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",
|
||||
|
11
src/App.tsx
11
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"
|
||||
@ -108,6 +110,7 @@ const router = createBrowserRouter([
|
||||
},
|
||||
{
|
||||
path: "/settings/api-keys",
|
||||
loader: getServerSettings,
|
||||
element: <SettingsAPIKeysRoute />,
|
||||
},
|
||||
{
|
||||
@ -167,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>
|
||||
|
@ -1,5 +1,6 @@
|
||||
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 {
|
||||
@ -13,12 +14,22 @@ export default async function createAPIKey({
|
||||
label,
|
||||
scopes,
|
||||
expiresAt,
|
||||
}: CreateAPIKeyData): Promise<APIKey> {
|
||||
const {data} = await client.post(`${import.meta.env.VITE_SERVER_BASE_URL}/v1/api-key`, {
|
||||
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)
|
||||
return parseAPIKey(data) as APIKey & {key: string}
|
||||
}
|
||||
|
@ -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",
|
||||
]
|
||||
|
@ -1,24 +1,53 @@
|
||||
import {ReactElement} from "react"
|
||||
import {APIKey} from "~/server-types"
|
||||
import {IconButton, ListItem, ListItemSecondaryAction, ListItemText} from "@mui/material"
|
||||
import {Alert, IconButton, ListItem, ListItemSecondaryAction, ListItemText} from "@mui/material"
|
||||
import {useTranslation} from "react-i18next"
|
||||
import {MdDelete} from "react-icons/md"
|
||||
import {MdContentCopy, MdDelete} from "react-icons/md"
|
||||
import {useCopyToClipboard} from "react-use"
|
||||
import {ErrorSnack, SuccessSnack} from "~/components"
|
||||
|
||||
export interface APIKeyListItemProps {
|
||||
apiKey: APIKey
|
||||
privateKey?: string
|
||||
}
|
||||
|
||||
export default function APIKeyListItem({apiKey}: APIKeyListItemProps): ReactElement {
|
||||
const {t} = useTranslation("settings-api-keys")
|
||||
export default function APIKeyListItem({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>
|
||||
<IconButton edge="end">
|
||||
<MdDelete />
|
||||
</IconButton>
|
||||
</ListItemSecondaryAction>
|
||||
<>
|
||||
<ListItemText primary={apiKey.label} secondary={apiKey.expiresAt.toString()} />
|
||||
<ListItemSecondaryAction>
|
||||
<IconButton edge="end">
|
||||
<MdDelete />
|
||||
</IconButton>
|
||||
</ListItemSecondaryAction>
|
||||
</>
|
||||
</ListItem>
|
||||
)
|
||||
}
|
||||
|
311
src/route-widgets/SettingsAPIKeysRoute/CreateNewAPIKeyDialog.tsx
Normal file
311
src/route-widgets/SettingsAPIKeysRoute/CreateNewAPIKeyDialog.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import {ReactElement} from "react"
|
||||
import {ReactElement, useState} from "react"
|
||||
import {useTranslation} from "react-i18next"
|
||||
import {useQuery} from "@tanstack/react-query"
|
||||
import {APIKey, PaginationResult} from "~/server-types"
|
||||
@ -6,43 +6,65 @@ import {AxiosError} from "axios"
|
||||
import {getAPIKeys} from "~/apis"
|
||||
import {QueryResult, SimplePage} from "~/components"
|
||||
import {Button, List} from "@mui/material"
|
||||
import {Link} from "react-router-dom"
|
||||
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 query = useQuery<PaginationResult<APIKey>, AxiosError>(["get_api_keys"], () =>
|
||||
getAPIKeys(),
|
||||
)
|
||||
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"
|
||||
component={Link}
|
||||
to="/settings/api-keys/new"
|
||||
>
|
||||
{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} />
|
||||
))}
|
||||
</List>
|
||||
) : (
|
||||
<EmptyStateScreen />
|
||||
)
|
||||
<>
|
||||
<SimplePage
|
||||
title={t("title")}
|
||||
actions={
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => setCreateNew(true)}
|
||||
startIcon={<MdAdd />}
|
||||
>
|
||||
{t("create.label")}
|
||||
</Button>
|
||||
}
|
||||
</QueryResult>
|
||||
</SimplePage>
|
||||
>
|
||||
<QueryResult<PaginationResult<APIKey>, AxiosError> query={query}>
|
||||
{({items: apiKeys}) =>
|
||||
apiKeys.length > 0 ? (
|
||||
<List>
|
||||
{apiKeys.map(apiKey => (
|
||||
<APIKeyListItem
|
||||
apiKey={apiKey}
|
||||
key={apiKey.id}
|
||||
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)
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -84,6 +84,7 @@ export interface ServerSettings {
|
||||
instanceSalt: string
|
||||
publicKey: string
|
||||
allowAliasDeletion: boolean
|
||||
apiKeyMaxDays: number
|
||||
}
|
||||
|
||||
export interface Alias {
|
||||
@ -141,11 +142,23 @@ export interface AliasList {
|
||||
type: AliasType
|
||||
}
|
||||
|
||||
export type APIKeyScope =
|
||||
| "profile_basic"
|
||||
| "full_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: string[]
|
||||
scopes: APIKeyScope[]
|
||||
}
|
||||
|
||||
export interface Report {
|
||||
|
97
yarn.lock
97
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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user