mirror of
https://github.com/Myzel394/kleckrelay-website.git
synced 2025-06-19 07:55:25 +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",
|
"@emotion/styled": "^11.10.4",
|
||||||
"@mui/lab": "^5.0.0-alpha.103",
|
"@mui/lab": "^5.0.0-alpha.103",
|
||||||
"@mui/material": "^5.10.9",
|
"@mui/material": "^5.10.9",
|
||||||
|
"@mui/x-date-pickers": "^6.0.1",
|
||||||
"@originjs/vite-plugin-commonjs": "^1.0.3",
|
"@originjs/vite-plugin-commonjs": "^1.0.3",
|
||||||
"@tanstack/react-query": "^4.12.0",
|
"@tanstack/react-query": "^4.12.0",
|
||||||
"axios": "^1.1.2",
|
"axios": "^1.1.2",
|
||||||
|
@ -59,6 +59,9 @@
|
|||||||
},
|
},
|
||||||
"report": {
|
"report": {
|
||||||
"deleted": "Report has been deleted!"
|
"deleted": "Report has been deleted!"
|
||||||
|
},
|
||||||
|
"apiKey": {
|
||||||
|
"keyCopied": "API key has been copied to your clipboard!"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"general": {
|
"general": {
|
||||||
@ -89,5 +92,22 @@
|
|||||||
"signup": "Sign up",
|
"signup": "Sign up",
|
||||||
"login": "Log in",
|
"login": "Log in",
|
||||||
"logout": "Log out"
|
"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",
|
"title": "Manage your API Keys",
|
||||||
"create": {
|
"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": {
|
"emptyState": {
|
||||||
"title": "Welcome to your API Keys",
|
"title": "Welcome to your API Keys",
|
||||||
|
@ -7,6 +7,8 @@ import React, {ReactElement} from "react"
|
|||||||
import {queryClient} from "~/constants/react-query"
|
import {queryClient} from "~/constants/react-query"
|
||||||
import {getServerSettings} from "~/apis"
|
import {getServerSettings} from "~/apis"
|
||||||
import {darkTheme, lightTheme} from "~/constants/themes"
|
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 AdminRoute from "~/routes/AdminRoute"
|
||||||
import AliasDetailRoute from "~/routes/AliasDetailRoute"
|
import AliasDetailRoute from "~/routes/AliasDetailRoute"
|
||||||
import AliasesRoute from "~/routes/AliasesRoute"
|
import AliasesRoute from "~/routes/AliasesRoute"
|
||||||
@ -108,6 +110,7 @@ const router = createBrowserRouter([
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/settings/api-keys",
|
path: "/settings/api-keys",
|
||||||
|
loader: getServerSettings,
|
||||||
element: <SettingsAPIKeysRoute />,
|
element: <SettingsAPIKeysRoute />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -167,9 +170,11 @@ export default function App(): ReactElement {
|
|||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<ThemeProvider theme={THEME_THEME_MAP[theme]}>
|
<ThemeProvider theme={THEME_THEME_MAP[theme]}>
|
||||||
<SnackbarProvider>
|
<SnackbarProvider>
|
||||||
|
<LocalizationProvider dateAdapter={AdapterDateFns}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
<I18nHandler />
|
<I18nHandler />
|
||||||
|
</LocalizationProvider>
|
||||||
</SnackbarProvider>
|
</SnackbarProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import {APIKey} from "~/server-types"
|
import {APIKey} from "~/server-types"
|
||||||
import {client} from "~/constants/axios-client"
|
import {client} from "~/constants/axios-client"
|
||||||
|
import formatISO from "date-fns/formatISO"
|
||||||
import parseAPIKey from "~/apis/helpers/parse-api-key"
|
import parseAPIKey from "~/apis/helpers/parse-api-key"
|
||||||
|
|
||||||
export interface CreateAPIKeyData {
|
export interface CreateAPIKeyData {
|
||||||
@ -13,12 +14,22 @@ export default async function createAPIKey({
|
|||||||
label,
|
label,
|
||||||
scopes,
|
scopes,
|
||||||
expiresAt,
|
expiresAt,
|
||||||
}: CreateAPIKeyData): Promise<APIKey> {
|
}: CreateAPIKeyData): Promise<APIKey & {key: string}> {
|
||||||
const {data} = await client.post(`${import.meta.env.VITE_SERVER_BASE_URL}/v1/api-key`, {
|
const {data} = await client.post(
|
||||||
|
`${import.meta.env.VITE_SERVER_BASE_URL}/v1/api-key`,
|
||||||
|
{
|
||||||
label,
|
label,
|
||||||
scopes,
|
scopes,
|
||||||
expiresAt,
|
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 ERROR_SNACKBAR_SHOW_DURATION = 5000
|
||||||
export const SUCCESS_SNACKBAR_SHOW_DURATION = 2000
|
export const SUCCESS_SNACKBAR_SHOW_DURATION = 2000
|
||||||
export const AUTHENTICATION_PATHS = ["/auth/login", "/auth/signup", "/auth/complete-account"]
|
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 {ReactElement} from "react"
|
||||||
import {APIKey} from "~/server-types"
|
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 {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 {
|
export interface APIKeyListItemProps {
|
||||||
apiKey: APIKey
|
apiKey: APIKey
|
||||||
|
privateKey?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function APIKeyListItem({apiKey}: APIKeyListItemProps): ReactElement {
|
export default function APIKeyListItem({apiKey, privateKey}: APIKeyListItemProps): ReactElement {
|
||||||
const {t} = useTranslation("settings-api-keys")
|
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 (
|
return (
|
||||||
<ListItem>
|
<ListItem>
|
||||||
|
<>
|
||||||
<ListItemText primary={apiKey.label} secondary={apiKey.expiresAt.toString()} />
|
<ListItemText primary={apiKey.label} secondary={apiKey.expiresAt.toString()} />
|
||||||
<ListItemSecondaryAction>
|
<ListItemSecondaryAction>
|
||||||
<IconButton edge="end">
|
<IconButton edge="end">
|
||||||
<MdDelete />
|
<MdDelete />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</ListItemSecondaryAction>
|
</ListItemSecondaryAction>
|
||||||
|
</>
|
||||||
</ListItem>
|
</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 {useTranslation} from "react-i18next"
|
||||||
import {useQuery} from "@tanstack/react-query"
|
import {useQuery} from "@tanstack/react-query"
|
||||||
import {APIKey, PaginationResult} from "~/server-types"
|
import {APIKey, PaginationResult} from "~/server-types"
|
||||||
@ -6,25 +6,29 @@ import {AxiosError} from "axios"
|
|||||||
import {getAPIKeys} from "~/apis"
|
import {getAPIKeys} from "~/apis"
|
||||||
import {QueryResult, SimplePage} from "~/components"
|
import {QueryResult, SimplePage} from "~/components"
|
||||||
import {Button, List} from "@mui/material"
|
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 APIKeyListItem from "~/route-widgets/SettingsAPIKeysRoute/APIKeyListItem"
|
||||||
|
import CreateNewAPIKeyDialog from "../route-widgets/SettingsAPIKeysRoute/CreateNewAPIKeyDialog"
|
||||||
import EmptyStateScreen from "~/route-widgets/SettingsAPIKeysRoute/EmptyStateScreen"
|
import EmptyStateScreen from "~/route-widgets/SettingsAPIKeysRoute/EmptyStateScreen"
|
||||||
|
|
||||||
export default function SettingsAPIKeysRoute(): ReactElement {
|
export default function SettingsAPIKeysRoute(): ReactElement {
|
||||||
const {t} = useTranslation("settings-api-keys")
|
const {t} = useTranslation("settings-api-keys")
|
||||||
const query = useQuery<PaginationResult<APIKey>, AxiosError>(["get_api_keys"], () =>
|
const queryKey = ["get_api_keys"]
|
||||||
getAPIKeys(),
|
const query = useQuery<PaginationResult<APIKey>, AxiosError>(queryKey, () => getAPIKeys())
|
||||||
)
|
|
||||||
|
const [createdAPIKey, setCreatedAPIKey] = useState<(APIKey & {key: string}) | null>(null)
|
||||||
|
const [createNew, setCreateNew] = useState<boolean>(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<SimplePage
|
<SimplePage
|
||||||
title={t("title")}
|
title={t("title")}
|
||||||
actions={
|
actions={
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
component={Link}
|
onClick={() => setCreateNew(true)}
|
||||||
to="/settings/api-keys/new"
|
startIcon={<MdAdd />}
|
||||||
>
|
>
|
||||||
{t("create.label")}
|
{t("create.label")}
|
||||||
</Button>
|
</Button>
|
||||||
@ -35,7 +39,15 @@ export default function SettingsAPIKeysRoute(): ReactElement {
|
|||||||
apiKeys.length > 0 ? (
|
apiKeys.length > 0 ? (
|
||||||
<List>
|
<List>
|
||||||
{apiKeys.map(apiKey => (
|
{apiKeys.map(apiKey => (
|
||||||
<APIKeyListItem apiKey={apiKey} key={apiKey.id} />
|
<APIKeyListItem
|
||||||
|
apiKey={apiKey}
|
||||||
|
key={apiKey.id}
|
||||||
|
privateKey={
|
||||||
|
apiKey.id === createdAPIKey?.id
|
||||||
|
? createdAPIKey.key
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</List>
|
</List>
|
||||||
) : (
|
) : (
|
||||||
@ -44,5 +56,15 @@ export default function SettingsAPIKeysRoute(): ReactElement {
|
|||||||
}
|
}
|
||||||
</QueryResult>
|
</QueryResult>
|
||||||
</SimplePage>
|
</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
|
instanceSalt: string
|
||||||
publicKey: string
|
publicKey: string
|
||||||
allowAliasDeletion: boolean
|
allowAliasDeletion: boolean
|
||||||
|
apiKeyMaxDays: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Alias {
|
export interface Alias {
|
||||||
@ -141,11 +142,23 @@ export interface AliasList {
|
|||||||
type: AliasType
|
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 {
|
export interface APIKey {
|
||||||
id: string
|
id: string
|
||||||
label: string
|
label: string
|
||||||
expiresAt: Date
|
expiresAt: Date
|
||||||
scopes: string[]
|
scopes: APIKeyScope[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Report {
|
export interface Report {
|
||||||
|
97
yarn.lock
97
yarn.lock
@ -464,6 +464,13 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
regenerator-runtime "^0.13.10"
|
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":
|
"@babel/template@^7.18.10":
|
||||||
version "7.18.10"
|
version "7.18.10"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.10.tgz#6f9134835970d1dbf0835c0d100c9f38de0c5e71"
|
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"
|
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
|
||||||
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
|
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":
|
"@emotion/babel-plugin@^11.10.0":
|
||||||
version "11.10.2"
|
version "11.10.2"
|
||||||
resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.10.2.tgz#879db80ba622b3f6076917a1e6f648b1c7d008c7"
|
resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.10.2.tgz#879db80ba622b3f6076917a1e6f648b1c7d008c7"
|
||||||
@ -1077,6 +1138,37 @@
|
|||||||
prop-types "^15.8.1"
|
prop-types "^15.8.1"
|
||||||
react-is "^18.2.0"
|
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":
|
"@nodelib/fs.scandir@2.1.5":
|
||||||
version "2.1.5"
|
version "2.1.5"
|
||||||
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
|
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"
|
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.10.tgz#ed07b19616bcbec5da6274ebc75ae95634bfc2ee"
|
||||||
integrity sha512-KepLsg4dU12hryUO7bp/axHAKvwGOCV0sGloQtpagJ12ai+ojVDqkeGSiRX1zlq+kjIMZ1t7gpze+26QqtdGqw==
|
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:
|
regexp.prototype.flags@^1.3.0, regexp.prototype.flags@^1.4.1, regexp.prototype.flags@^1.4.3:
|
||||||
version "1.4.3"
|
version "1.4.3"
|
||||||
resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac"
|
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