From 830d6f17d53acc12ef6d5b605c4a16ccb7976dee Mon Sep 17 00:00:00 2001 From: Tom Jeannesson Date: Sun, 15 Oct 2023 13:46:25 +0200 Subject: [PATCH] feat(servers-&&-keys): fulls integrated --- desktop-app/package.json | 1 + desktop-app/renderer/api/key/key.ts | 54 +++- .../renderer/components/custom/copyButton.tsx | 5 +- .../renderer/components/ui/alert-dialog.tsx | 141 +++++++++ .../renderer/pages/keys/[slug]/index.tsx | 41 ++- .../keys/[slug]/keyGeneralAttributes.tsx | 179 +++++++++++ .../pages/keys/[slug]/keyPermissions.tsx | 184 +++++++++++ .../pages/servers/[slug]/adminFooter.tsx | 287 +++++++++++------- .../pages/servers/[slug]/selectedAPIKey.tsx | 9 +- desktop-app/yarn.lock | 15 +- 10 files changed, 775 insertions(+), 141 deletions(-) create mode 100644 desktop-app/renderer/components/ui/alert-dialog.tsx create mode 100644 desktop-app/renderer/pages/keys/[slug]/keyGeneralAttributes.tsx create mode 100644 desktop-app/renderer/pages/keys/[slug]/keyPermissions.tsx diff --git a/desktop-app/package.json b/desktop-app/package.json index b38a2377..a3a5c462 100644 --- a/desktop-app/package.json +++ b/desktop-app/package.json @@ -15,6 +15,7 @@ "dependencies": { "@hookform/resolvers": "^3.3.0", "@radix-ui/react-accordion": "^1.1.2", + "@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.5", diff --git a/desktop-app/renderer/api/key/key.ts b/desktop-app/renderer/api/key/key.ts index b7e809f7..81a7b092 100644 --- a/desktop-app/renderer/api/key/key.ts +++ b/desktop-app/renderer/api/key/key.ts @@ -13,9 +13,14 @@ export interface Key { export async function getKey( searchParams: ReturnType, - id: string + id: string, + space: string | null = null ): Promise> { - const response = await request(searchParams, 'GET', `/api/key/${id}/`) + const response = await request( + searchParams, + 'GET', + space ? `/api/key/${id}/?space=${space}` : `/api/key/${id}/` + ) return response as AxiosResponse } export async function listKey( @@ -41,3 +46,48 @@ export async function createKey( }) return response as AxiosResponse<{ key: string }> } + +export async function deleteKey( + searchParams: ReturnType, + id: string +): Promise> { + const response = await request(searchParams, 'DELETE', `/api/key/${id}/`) + return response as AxiosResponse +} + +export async function possiblePermissions( + searchParams: ReturnType +): Promise> { + const response = await request( + searchParams, + 'GET', + '/api/key/possible_permissions/' + ) + return response as AxiosResponse +} + +export async function updateKey( + searchParams: ReturnType, + id: string, + name?: string, + description?: string, + revoked?: boolean, + permissions?: string[], + space?: string +): Promise> { + if (permissions && !space) { + throw new Error('space is required when updating permissions') + } + const response = await request( + searchParams, + 'PATCH', + space ? `/api/key/${id}/?space=${space}` : `/api/key/${id}/`, + { + name, + description, + revoked, + permissions + } + ) + return response as AxiosResponse +} diff --git a/desktop-app/renderer/components/custom/copyButton.tsx b/desktop-app/renderer/components/custom/copyButton.tsx index a05a70b1..03dfe177 100644 --- a/desktop-app/renderer/components/custom/copyButton.tsx +++ b/desktop-app/renderer/components/custom/copyButton.tsx @@ -7,9 +7,10 @@ import { cn } from '@/lib/utils' interface CopyButtonProps extends React.HTMLAttributes { value: string src?: string + copyTrigger: () => void } -export async function copyToClipboardWithMeta(value: string, event?: Event) { +export async function copyToClipboardWithMeta(value: string) { navigator.clipboard.writeText(value) } @@ -17,6 +18,7 @@ export default function CopyButton({ value, className, src, + copyTrigger, ...props }: CopyButtonProps) { const [hasCopied, setHasCopied] = React.useState(false) @@ -38,6 +40,7 @@ export default function CopyButton({ onClick={() => { copyToClipboardWithMeta(value) setHasCopied(true) + copyTrigger() }} {...props} > diff --git a/desktop-app/renderer/components/ui/alert-dialog.tsx b/desktop-app/renderer/components/ui/alert-dialog.tsx new file mode 100644 index 00000000..c778fbd6 --- /dev/null +++ b/desktop-app/renderer/components/ui/alert-dialog.tsx @@ -0,0 +1,141 @@ +'use client' + +import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog' +import * as React from 'react' + +import { buttonVariants } from '@/components/ui/button' +import { cn } from '@/lib/utils' + +const AlertDialog = AlertDialogPrimitive.Root + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger + +const AlertDialogPortal = AlertDialogPrimitive.Portal + +const AlertDialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +)) +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName + +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + +)) +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName + +const AlertDialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogHeader.displayName = 'AlertDialogHeader' + +const AlertDialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogFooter.displayName = 'AlertDialogFooter' + +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName + +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogDescription.displayName = + AlertDialogPrimitive.Description.displayName + +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName + +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName + +export { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogOverlay, + AlertDialogPortal, + AlertDialogTitle, + AlertDialogTrigger +} diff --git a/desktop-app/renderer/pages/keys/[slug]/index.tsx b/desktop-app/renderer/pages/keys/[slug]/index.tsx index 6d372ca6..1afc850e 100644 --- a/desktop-app/renderer/pages/keys/[slug]/index.tsx +++ b/desktop-app/renderer/pages/keys/[slug]/index.tsx @@ -1,24 +1,33 @@ +import { Key } from '@/api/key/key' import ContextHeader from '@/components/layout/contextHeader' -import { Server, getServer } from '@/lib/localStorage' -import { useSearchParams } from 'next/navigation' -import { useEffect, useState } from 'react' -export default function Servers(): JSX.Element { - const searchParams = useSearchParams() - const [server, setServer] = useState( - getServer(searchParams.get('server') || '') - ) - useEffect( - () => setServer(getServer(searchParams.get('server') || '')), - [searchParams] - ) +import KeyGeneralAttributes from './keyGeneralAttributes' +import KeyPermissions from './keyPermissions' + +const defaultKey: Key = { + name: '', + prefix: '', + permissions: [], + is_master_key: false, + revoked: false, + description: '' +} +export default function Key(): JSX.Element { return (
-

- Settings - Servers -

-

Here is where you can manage your servers.

+
+

+ Settings - Key +

+

+ Here is where you can manage your distibuted API keys. +

+
+
+ + +
) diff --git a/desktop-app/renderer/pages/keys/[slug]/keyGeneralAttributes.tsx b/desktop-app/renderer/pages/keys/[slug]/keyGeneralAttributes.tsx new file mode 100644 index 00000000..c16e0372 --- /dev/null +++ b/desktop-app/renderer/pages/keys/[slug]/keyGeneralAttributes.tsx @@ -0,0 +1,179 @@ +import { Key, deleteKey, getKey, updateKey } from '@/api/key/key' +import { Button } from '@/components/ui/button' +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle +} from '@/components/ui/card' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Switch } from '@/components/ui/switch' +import { useToast } from '@/components/ui/use-toast' +import { standardUrlPartial } from '@/lib/queryParams' +import { AxiosResponse } from 'axios' +import { useSearchParams } from 'next/navigation' +import { useRouter } from 'next/router' +import { useEffect, useState } from 'react' + +const defaultKey: Key = { + name: '', + prefix: '', + permissions: [], + is_master_key: false, + revoked: false, + description: '' +} + +export default function KeyGeneralAttributes(): JSX.Element { + const searchParams = useSearchParams() + const router = useRouter() + const { toast } = useToast() + const [key, setKey] = useState(defaultKey) + const [revoked, setRevoked] = useState(false) + useEffect(() => { + const fetchData = async () => { + try { + const response: AxiosResponse = await getKey( + searchParams, + router.query.slug as string + ) + setKey(response.data) + setRevoked(response.data.revoked) + } catch (error) { + console.log(error) + setKey(defaultKey) + } + } + if (searchParams.get('server')) { + fetchData() + } + }, [searchParams, router]) + + return ( + + +
+ Your key: +
+ + You can view your API key properties here. + +
+
+ +
+
+
+ + { + setKey({ ...key, name: e.currentTarget.value }) + }} + /> +
+
+ + { + setKey({ ...key, description: e.currentTarget.value }) + }} + /> +
+
+ + +
+
+ + +
+
+ + { + setKey({ ...key, revoked: !key.revoked }) + }} + /> +
+
+
+
+ + + + +
+
+ ) +} diff --git a/desktop-app/renderer/pages/keys/[slug]/keyPermissions.tsx b/desktop-app/renderer/pages/keys/[slug]/keyPermissions.tsx new file mode 100644 index 00000000..153269ca --- /dev/null +++ b/desktop-app/renderer/pages/keys/[slug]/keyPermissions.tsx @@ -0,0 +1,184 @@ +import { Key, getKey, possiblePermissions, updateKey } from '@/api/key/key' +import { NapseSpace, listSpace } from '@/api/spaces/spaces' +import { Button } from '@/components/ui/button' +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle +} from '@/components/ui/card' +import { Label } from '@/components/ui/label' +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue +} from '@/components/ui/select' +import { Switch } from '@/components/ui/switch' +import { useToast } from '@/components/ui/use-toast' +import { AxiosResponse } from 'axios' +import { useSearchParams } from 'next/navigation' +import { useRouter } from 'next/router' +import { useEffect, useState } from 'react' + +const defaultKey: Key = { + name: '', + prefix: '', + permissions: [], + is_master_key: false, + revoked: false, + description: '' +} + +export default function KeyPermissions(): JSX.Element { + const searchParams = useSearchParams() + const router = useRouter() + const { toast } = useToast() + const [key, setKey] = useState(defaultKey) + const [permissionArray, setPermissionArray] = useState([]) + const [currentPermissions, setCurrentPermissions] = useState([]) + + const [spaces, setSpaces] = useState([]) + useEffect(() => { + const fetchData = async () => { + try { + const response: AxiosResponse = + await listSpace(searchParams) + setSpaces(response.data) + const response2: AxiosResponse = + await possiblePermissions(searchParams) + setPermissionArray(response2.data) + } catch (error) { + console.log(error) + setSpaces([]) + } + } + if (searchParams.get('server')) { + fetchData() + } + }, [searchParams, router]) + const [selectedSpace, setSelectedSpace] = useState('') + + useEffect(() => { + const fetchData = async () => { + if (!selectedSpace) { + return + } + try { + const response: AxiosResponse = await getKey( + searchParams, + router.query.slug as string, + selectedSpace + ) + setCurrentPermissions(response.data.permissions) + setKey(response.data) + } catch (error) { + console.log(error) + setSelectedSpace('') + } + } + fetchData() + }, [selectedSpace, router, searchParams]) + return ( + + +
+ Key Permissions: +
+ + You can view your API key permissions here. Please select a space to + view permissions. + + +
+
+ +
+ {selectedSpace && ( + <> + + {permissionArray.map((permission, index) => ( +
+ { + if (key.is_master_key) { + return + } + if (currentPermissions.includes(permission)) { + setCurrentPermissions( + currentPermissions.filter( + (item) => item !== permission + ) + ) + return + } + setCurrentPermissions([ + ...currentPermissions, + permission + ]) + }} + /> +

{permission}

+
+ ))} + + )} +
+
+ + + +
+
+ ) +} diff --git a/desktop-app/renderer/pages/servers/[slug]/adminFooter.tsx b/desktop-app/renderer/pages/servers/[slug]/adminFooter.tsx index 5c19ac55..3fe2942d 100644 --- a/desktop-app/renderer/pages/servers/[slug]/adminFooter.tsx +++ b/desktop-app/renderer/pages/servers/[slug]/adminFooter.tsx @@ -1,6 +1,17 @@ -import { createKey } from '@/api/key/key' +import { createKey, deleteKey } from '@/api/key/key' import CopyButton from '@/components/custom/copyButton' -import { Button } from '@/components/ui/button' +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger +} from '@/components/ui/alert-dialog' +import { Button, buttonVariants } from '@/components/ui/button' import { CardFooter } from '@/components/ui/card' import { Dialog, @@ -29,128 +40,176 @@ export default function AdminFooter({}): JSX.Element { defaultAPIKeyDescription ) const [fullAPIKey, setFullAPIKey] = useState('') + const [hasCopied, setHasCopied] = useState(false) return ( - - - - - - - - Create New API Key - - An API key is a unique identifier used to interact with the - server. You can create a new API key by providing a name and - description. Before you can use it, you then need to give it - Space-Specific permissions. Be careful who you give your API key - to. - - -
-
- - { - setAPIKeyName(e.currentTarget.value) - }} - /> -
-
- - { - setAPIKeyDescription(e.currentTarget.value) - }} - /> -
-
- {fullAPIKey && ( - <> + <> + + + + + + + + Create New API Key - Here is the API key you just created. You can copy it and use it - to interact with the server. If you lose it, or leave this page - without copying it, you will need to create a new one. + An API key is a unique identifier used to interact with the + server. You can create a new API key by providing a name and + description. Before you can use it, you then need to give it + Space-Specific permissions. Be careful who you give your API key + to. + +
-
+
+ + { + setAPIKeyDescription(e.currentTarget.value) + }} + />
- - )} - -
- - {fullAPIKey && ( +
+ {fullAPIKey && ( + <> + + Here is the API key you just created. You can copy it and use + it to interact with the server. If you lose it, or leave this + page without copying it, you will need to create a new one. + +
+ +
+ { + setAPIKeyDescription(e.currentTarget.value) + }} + /> + + { + setHasCopied(true) + }} + /> +
+
+ + )} + +
- )} -
-
- -
-
+ {fullAPIKey && ( + + + + + {!hasCopied && ( + + + Are you sure ? + + You have not copied your API key. If you leave this + page without copying it, it will be deleted, and you + will need to create a new one. + + + + Cancel + { + try { + await deleteKey( + searchParams, + fullAPIKey.split('.')[0] + ) + setFullAPIKey('') + } catch (err) { + console.error(err) + } + }} + > + Continue + + + + )} + + )} +
+ + + + + {} + ) } diff --git a/desktop-app/renderer/pages/servers/[slug]/selectedAPIKey.tsx b/desktop-app/renderer/pages/servers/[slug]/selectedAPIKey.tsx index bbe72fef..8a7cd0f2 100644 --- a/desktop-app/renderer/pages/servers/[slug]/selectedAPIKey.tsx +++ b/desktop-app/renderer/pages/servers/[slug]/selectedAPIKey.tsx @@ -178,13 +178,8 @@ export default function SelectedAPIKey({ />
- - + +
{key.permissions.length > 0 && (
diff --git a/desktop-app/yarn.lock b/desktop-app/yarn.lock index 62531d16..d44cc0be 100644 --- a/desktop-app/yarn.lock +++ b/desktop-app/yarn.lock @@ -1574,6 +1574,19 @@ "@radix-ui/react-primitive" "1.0.3" "@radix-ui/react-use-controllable-state" "1.0.1" +"@radix-ui/react-alert-dialog@^1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.0.5.tgz#70dd529cbf1e4bff386814d3776901fcaa131b8c" + integrity sha512-OrVIOcZL0tl6xibeuGt5/+UxoT2N27KCFOPjFyfXMnchxSHZ/OW7cCX2nGlIYJrbHK/fczPcFzAwvNBB6XBNMA== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.1" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-dialog" "1.0.5" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-slot" "1.0.2" + "@radix-ui/react-arrow@1.0.3": version "1.0.3" resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.0.3.tgz#c24f7968996ed934d57fe6cde5d6ec7266e1d25d" @@ -1637,7 +1650,7 @@ dependencies: "@babel/runtime" "^7.13.10" -"@radix-ui/react-dialog@^1.0.5": +"@radix-ui/react-dialog@1.0.5", "@radix-ui/react-dialog@^1.0.5": version "1.0.5" resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.0.5.tgz#71657b1b116de6c7a0b03242d7d43e01062c7300" integrity sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==