From 157e075885318239ae15b20d4a8c2d3ef619d51d Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Fri, 10 Mar 2023 21:34:24 +0100 Subject: [PATCH 1/8] fix: typo --- public/locales/en-US/signup.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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?", From bc5063b44c7b9100ee907e8452c9d0662f1ae4be Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Fri, 10 Mar 2023 21:38:16 +0100 Subject: [PATCH 2/8] feat(api-keys): Add to settings --- public/locales/en-US/settings.json | 3 ++- src/routes/SettingsRoute.tsx | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/public/locales/en-US/settings.json b/public/locales/en-US/settings.json index 7ab1855..3f70247 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": "Edit API Keys" } } 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 { + + + + + + ) From 7cd798a3432ddfbc0b019edaa10554e23cc229fb Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Fri, 10 Mar 2023 22:55:20 +0100 Subject: [PATCH 3/8] feat(api-keys): Add APIs --- src/apis/create-api-key.ts | 24 ++++++++++++++++++++++++ src/apis/delete-api-key.ts | 9 +++++++++ src/apis/get-api-keys.ts | 22 ++++++++++++++++++++++ src/apis/helpers/parse-api-key.ts | 8 ++++++++ src/apis/index.ts | 6 ++++++ 5 files changed, 69 insertions(+) create mode 100644 src/apis/create-api-key.ts create mode 100644 src/apis/delete-api-key.ts create mode 100644 src/apis/get-api-keys.ts create mode 100644 src/apis/helpers/parse-api-key.ts diff --git a/src/apis/create-api-key.ts b/src/apis/create-api-key.ts new file mode 100644 index 0000000..e44558f --- /dev/null +++ b/src/apis/create-api-key.ts @@ -0,0 +1,24 @@ +import {APIKey} from "~/server-types" +import {client} from "~/constants/axios-client" +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, + }) + + return parseAPIKey(data) +} diff --git a/src/apis/delete-api-key.ts b/src/apis/delete-api-key.ts new file mode 100644 index 0000000..bc2ba38 --- /dev/null +++ b/src/apis/delete-api-key.ts @@ -0,0 +1,9 @@ +import {client} from "~/constants/axios-client" + +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" From eeb6ba48927e0b8badd04628a66d869d3922e848 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Fri, 10 Mar 2023 22:55:27 +0100 Subject: [PATCH 4/8] feat(api-keys): Add API overview --- public/locales/en-US/settings-api-keys.json | 10 ++++ public/locales/en-US/settings.json | 2 +- src/App.tsx | 5 ++ .../SettingsAPIKeysRoute/APIKeyListItem.tsx | 24 ++++++++++ .../SettingsAPIKeysRoute/EmptyStateScreen.tsx | 35 ++++++++++++++ src/routes/SettingsAPIKeysRoute.tsx | 48 +++++++++++++++++++ src/server-types.ts | 7 +++ 7 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 public/locales/en-US/settings-api-keys.json create mode 100644 src/route-widgets/SettingsAPIKeysRoute/APIKeyListItem.tsx create mode 100644 src/route-widgets/SettingsAPIKeysRoute/EmptyStateScreen.tsx create mode 100644 src/routes/SettingsAPIKeysRoute.tsx 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..fe1a5d7 --- /dev/null +++ b/public/locales/en-US/settings-api-keys.json @@ -0,0 +1,10 @@ +{ + "title": "Manage your API Keys", + "create": { + "label": "Create a new API Key" + }, + "emptyState": { + "title": "Welcome to your API Keys", + "description": "Create an API Key to get started with the API." + } +} diff --git a/public/locales/en-US/settings.json b/public/locales/en-US/settings.json index 3f70247..756d4aa 100644 --- a/public/locales/en-US/settings.json +++ b/public/locales/en-US/settings.json @@ -3,6 +3,6 @@ "actions": { "enable2fa": "Two-Factor-Authentication", "aliasPreferences": "Alias Preferences", - "apiKeys": "Edit API Keys" + "apiKeys": "Manage API Keys" } } diff --git a/src/App.tsx b/src/App.tsx index c54ecea..c58c86c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -28,6 +28,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 +106,10 @@ const router = createBrowserRouter([ path: "/settings/2fa", element: , }, + { + path: "/settings/api-keys", + element: , + }, { path: "/reports", loader: getServerSettings, diff --git a/src/route-widgets/SettingsAPIKeysRoute/APIKeyListItem.tsx b/src/route-widgets/SettingsAPIKeysRoute/APIKeyListItem.tsx new file mode 100644 index 0000000..93e9783 --- /dev/null +++ b/src/route-widgets/SettingsAPIKeysRoute/APIKeyListItem.tsx @@ -0,0 +1,24 @@ +import {ReactElement} from "react" +import {APIKey} from "~/server-types" +import {IconButton, ListItem, ListItemSecondaryAction, ListItemText} from "@mui/material" +import {useTranslation} from "react-i18next" +import {MdDelete} from "react-icons/md" + +export interface APIKeyListItemProps { + apiKey: APIKey +} + +export default function APIKeyListItem({apiKey}: APIKeyListItemProps): ReactElement { + const {t} = useTranslation("settings-api-keys") + + return ( + + + + + + + + + ) +} 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..82d9ab8 --- /dev/null +++ b/src/routes/SettingsAPIKeysRoute.tsx @@ -0,0 +1,48 @@ +import {ReactElement} 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 {Link} from "react-router-dom" +import APIKeyListItem from "~/route-widgets/SettingsAPIKeysRoute/APIKeyListItem" +import EmptyStateScreen from "~/route-widgets/SettingsAPIKeysRoute/EmptyStateScreen" + +export default function SettingsAPIKeysRoute(): ReactElement { + const {t} = useTranslation("settings-api-keys") + const query = useQuery, AxiosError>(["get_api_keys"], () => + getAPIKeys(), + ) + + return ( + + {t("create.label")} + + } + > + , AxiosError> query={query}> + {({items: apiKeys}) => + apiKeys.length > 0 ? ( + + {apiKeys.map(apiKey => ( + + ))} + + ) : ( + + ) + } + + + ) +} diff --git a/src/server-types.ts b/src/server-types.ts index 62642c7..6046098 100644 --- a/src/server-types.ts +++ b/src/server-types.ts @@ -141,6 +141,13 @@ export interface AliasList { type: AliasType } +export interface APIKey { + id: string + label: string + expiresAt: Date + scopes: string[] +} + export interface Report { id: string encryptedContent: string From dace44f1d90b19c64592bfeda4e8a8fab6cb7467 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 11 Mar 2023 12:07:12 +0100 Subject: [PATCH 5/8] feat(api-key): Add create api key functionality --- package.json | 1 + public/locales/en-US/common.json | 20 ++ public/locales/en-US/settings-api-keys.json | 27 +- src/App.tsx | 11 +- src/apis/create-api-key.ts | 25 +- src/constants/values.ts | 15 + .../SettingsAPIKeysRoute/APIKeyListItem.tsx | 49 ++- .../CreateNewAPIKeyDialog.tsx | 311 ++++++++++++++++++ src/routes/SettingsAPIKeysRoute.tsx | 84 +++-- src/server-types.ts | 15 +- yarn.lock | 97 ++++++ 11 files changed, 602 insertions(+), 53 deletions(-) create mode 100644 src/route-widgets/SettingsAPIKeysRoute/CreateNewAPIKeyDialog.tsx 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..3fb6c83 100644 --- a/public/locales/en-US/common.json +++ b/public/locales/en-US/common.json @@ -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" + } } } diff --git a/public/locales/en-US/settings-api-keys.json b/public/locales/en-US/settings-api-keys.json index fe1a5d7..e2e5623 100644 --- a/public/locales/en-US/settings-api-keys.json +++ b/public/locales/en-US/settings-api-keys.json @@ -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", diff --git a/src/App.tsx b/src/App.tsx index c58c86c..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" @@ -108,6 +110,7 @@ const router = createBrowserRouter([ }, { path: "/settings/api-keys", + loader: getServerSettings, element: , }, { @@ -167,9 +170,11 @@ export default function App(): ReactElement { - - - + + + + + diff --git a/src/apis/create-api-key.ts b/src/apis/create-api-key.ts index e44558f..ac7582c 100644 --- a/src/apis/create-api-key.ts +++ b/src/apis/create-api-key.ts @@ -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 { - const {data} = await client.post(`${import.meta.env.VITE_SERVER_BASE_URL}/v1/api-key`, { - 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) + return parseAPIKey(data) as APIKey & {key: string} } 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 index 93e9783..c410a51 100644 --- a/src/route-widgets/SettingsAPIKeysRoute/APIKeyListItem.tsx +++ b/src/route-widgets/SettingsAPIKeysRoute/APIKeyListItem.tsx @@ -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 ( + <> + + + {privateKey} + + copy(privateKey)}> + + + + + + + + + ) + } return ( - - - - - - + <> + + + + + + + ) } 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/routes/SettingsAPIKeysRoute.tsx b/src/routes/SettingsAPIKeysRoute.tsx index 82d9ab8..684e1f5 100644 --- a/src/routes/SettingsAPIKeysRoute.tsx +++ b/src/routes/SettingsAPIKeysRoute.tsx @@ -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, AxiosError>(["get_api_keys"], () => - getAPIKeys(), - ) + 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 ( - - {t("create.label")} - - } - > - , AxiosError> query={query}> - {({items: apiKeys}) => - apiKeys.length > 0 ? ( - - {apiKeys.map(apiKey => ( - - ))} - - ) : ( - - ) + <> + 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/server-types.ts b/src/server-types.ts index 6046098..a46d565 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,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 { 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" From 9605fafe54fd0f72b820b48435c867533fb9aea4 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 11 Mar 2023 12:42:04 +0100 Subject: [PATCH 6/8] feat(api-key): Add delete functionality --- public/locales/en-US/common.json | 3 +- public/locales/en-US/settings-api-keys.json | 7 ++++ src/apis/delete-api-key.ts | 3 +- src/components/widgets/DeleteAPIButton.tsx | 35 +++++++++++++------ .../SettingsAPIKeysRoute/APIKeyListItem.tsx | 32 ++++++++++++++--- 5 files changed, 62 insertions(+), 18 deletions(-) diff --git a/public/locales/en-US/common.json b/public/locales/en-US/common.json index 3fb6c83..74f89d5 100644 --- a/public/locales/en-US/common.json +++ b/public/locales/en-US/common.json @@ -61,7 +61,8 @@ "deleted": "Report has been deleted!" }, "apiKey": { - "keyCopied": "API key has been copied to your clipboard!" + "keyCopied": "API key has been copied to your clipboard!", + "deleted": "API key has been deleted!" } }, "general": { diff --git a/public/locales/en-US/settings-api-keys.json b/public/locales/en-US/settings-api-keys.json index e2e5623..e909f35 100644 --- a/public/locales/en-US/settings-api-keys.json +++ b/public/locales/en-US/settings-api-keys.json @@ -31,5 +31,12 @@ "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/src/apis/delete-api-key.ts b/src/apis/delete-api-key.ts index bc2ba38..9280592 100644 --- a/src/apis/delete-api-key.ts +++ b/src/apis/delete-api-key.ts @@ -1,6 +1,7 @@ import {client} from "~/constants/axios-client" +import {SimpleDetailResponse} from "~/server-types" -export default async function deleteApiKey(id: string): Promise { +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, }) 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/route-widgets/SettingsAPIKeysRoute/APIKeyListItem.tsx b/src/route-widgets/SettingsAPIKeysRoute/APIKeyListItem.tsx index c410a51..760346a 100644 --- a/src/route-widgets/SettingsAPIKeysRoute/APIKeyListItem.tsx +++ b/src/route-widgets/SettingsAPIKeysRoute/APIKeyListItem.tsx @@ -4,14 +4,21 @@ import {Alert, IconButton, ListItem, ListItemSecondaryAction, ListItemText} from import {useTranslation} from "react-i18next" import {MdContentCopy, MdDelete} from "react-icons/md" import {useCopyToClipboard} from "react-use" -import {ErrorSnack, SuccessSnack} from "~/components" +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({apiKey, privateKey}: APIKeyListItemProps): ReactElement { +export default function APIKeyListItem({ + queryKey, + apiKey, + privateKey, +}: APIKeyListItemProps): ReactElement { const {t} = useTranslation(["settings-api-keys", "common"]) const [{value, error}, copy] = useCopyToClipboard() @@ -43,9 +50,24 @@ export default function APIKeyListItem({apiKey, privateKey}: APIKeyListItemProps <> - - - + 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 => ( + + + + )} + From 1d195ff28d309aaeaf0904c51a70d77ee06b0ac8 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 11 Mar 2023 12:49:16 +0100 Subject: [PATCH 7/8] fix(api-key): Fix type --- src/server-types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server-types.ts b/src/server-types.ts index a46d565..9f8535e 100644 --- a/src/server-types.ts +++ b/src/server-types.ts @@ -143,8 +143,8 @@ export interface AliasList { } export type APIKeyScope = - | "profile_basic" | "full_profile" + | "basic_profile" | "read:preferences" | "update:preferences" | "read:alias" From 1c0f5b8738286e8d7a32fe4df95f313bcd5560b5 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 11 Mar 2023 12:53:26 +0100 Subject: [PATCH 8/8] fix(api-key): Add missing prop --- src/routes/SettingsAPIKeysRoute.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/routes/SettingsAPIKeysRoute.tsx b/src/routes/SettingsAPIKeysRoute.tsx index 684e1f5..922f4b2 100644 --- a/src/routes/SettingsAPIKeysRoute.tsx +++ b/src/routes/SettingsAPIKeysRoute.tsx @@ -42,6 +42,7 @@ export default function SettingsAPIKeysRoute(): ReactElement {