diff --git a/web/src/pages/UsersOverviewPage/UsersTable.tsx b/web/src/pages/UsersOverviewPage/UsersTable.tsx index 1e0114c116..a343c1432c 100644 --- a/web/src/pages/UsersOverviewPage/UsersTable.tsx +++ b/web/src/pages/UsersOverviewPage/UsersTable.tsx @@ -439,6 +439,7 @@ export const UsersTable = () => { device: Device, username: string, reservedDeviceNames: string[], + reservedPubkeys: string[], ): MenuItemsGroup[] => [ { items: [ @@ -449,6 +450,7 @@ export const UsersTable = () => { openModal(ModalName.EditUserDevice, { device, reservedNames: reservedDeviceNames, + reservedPubkeys, username, }); }, @@ -508,6 +510,7 @@ export const UsersTable = () => { (row: Row, isLast = false) => { const username = row.original.username; const reservedDeviceNames = row.original.devices.map((d) => d.name); + const reservedPubkeys = row.original.devices.map((d) => d.wireguard_pubkey); return row.original.devices.map((device, deviceIndex) => { const lastRow = isLast && deviceIndex === row.original.devices.length - 1; const latestNetwork = orderBy( @@ -523,7 +526,12 @@ export const UsersTable = () => { const connectionDate = latestNetwork?.last_connected_at ? displayDate(latestNetwork.last_connected_at) : neverConnected; - const menuItems = makeDeviceRowMenu(device, username, reservedDeviceNames); + const menuItems = makeDeviceRowMenu( + device, + username, + reservedDeviceNames, + reservedPubkeys, + ); return ( { id: rowData.id, name: rowData.name, username, + reservedNames: mapped.map((k) => k.name), }); }, }, @@ -198,7 +199,7 @@ export const ProfileAuthKeysTable = () => { }, }), ], - [deleteAuthKey, username, writeToClipboard], + [deleteAuthKey, username, writeToClipboard, mapped.map], ); const table = useReactTable({ diff --git a/web/src/pages/user-profile/UserProfilePage/tabs/ProfileAuthKeysTab/modals/RenameAuthKeyModal/RenameAuthKeyModal.tsx b/web/src/pages/user-profile/UserProfilePage/tabs/ProfileAuthKeysTab/modals/RenameAuthKeyModal/RenameAuthKeyModal.tsx index 15b85b119b..34f2fde39c 100644 --- a/web/src/pages/user-profile/UserProfilePage/tabs/ProfileAuthKeysTab/modals/RenameAuthKeyModal/RenameAuthKeyModal.tsx +++ b/web/src/pages/user-profile/UserProfilePage/tabs/ProfileAuthKeysTab/modals/RenameAuthKeyModal/RenameAuthKeyModal.tsx @@ -1,6 +1,6 @@ import { useStore } from '@tanstack/react-form'; import { useMutation } from '@tanstack/react-query'; -import { useEffect, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import z from 'zod'; import { m } from '../../../../../../../paraglide/messages'; import api from '../../../../../../../shared/api/api'; @@ -50,11 +50,21 @@ export const RenameAuthKeyModal = () => { ); }; -const formSchema = z.object({ - name: z.string().trim().min(1, m.form_error_required()), -}); +const getFormSchema = (reservedNames: string[]) => + z.object({ + name: z + .string() + .trim() + .min(1, m.form_error_required()) + .refine((val) => !reservedNames.includes(val), m.form_error_name_reserved()), + }); + +const ModalContent = ({ id, name, username, reservedNames }: OpenAuthKeyRenameModal) => { + const formSchema = useMemo( + () => getFormSchema(reservedNames.filter((n) => n !== name)), + [reservedNames, name], + ); -const ModalContent = ({ id, name, username }: OpenAuthKeyRenameModal) => { const { mutateAsync } = useMutation({ mutationFn: api.user.renameAuthKey, meta: { diff --git a/web/src/pages/user-profile/UserProfilePage/tabs/ProfileDevicesTab/components/ProfileDevicesTable/ProfileDevicesTable.tsx b/web/src/pages/user-profile/UserProfilePage/tabs/ProfileDevicesTab/components/ProfileDevicesTable/ProfileDevicesTable.tsx index a8047c69e0..a775dd3bf4 100644 --- a/web/src/pages/user-profile/UserProfilePage/tabs/ProfileDevicesTab/components/ProfileDevicesTable/ProfileDevicesTable.tsx +++ b/web/src/pages/user-profile/UserProfilePage/tabs/ProfileDevicesTab/components/ProfileDevicesTable/ProfileDevicesTable.tsx @@ -93,6 +93,10 @@ const DevicesTable = ({ rowData }: { rowData: RowData[] }) => { const username = user.username; const reservedNames = useMemo(() => rowData.map((row) => row.name), [rowData]); + const reservedPubkeys = useMemo( + () => rowData.map((row) => row.wireguard_pubkey), + [rowData], + ); const addDeviceProps = useMemo( (): ButtonProps => ({ @@ -121,6 +125,7 @@ const DevicesTable = ({ rowData }: { rowData: RowData[] }) => { openModal(ModalName.EditUserDevice, { device: row, reservedNames: reservedNames, + reservedPubkeys: reservedPubkeys, username, }); }, @@ -177,7 +182,7 @@ const DevicesTable = ({ rowData }: { rowData: RowData[] }) => { ); return [{ items }]; }, - [reservedNames, username, isAdmin], + [reservedNames, username, isAdmin, reservedPubkeys], ); const tableColumns = useMemo( diff --git a/web/src/shared/components/modals/AddUserDeviceModal/steps/AddDeviceModalManualSetupStep/AddDeviceModalManualSetupStep.tsx b/web/src/shared/components/modals/AddUserDeviceModal/steps/AddDeviceModalManualSetupStep/AddDeviceModalManualSetupStep.tsx index 2b178dbae5..36203d2816 100644 --- a/web/src/shared/components/modals/AddUserDeviceModal/steps/AddDeviceModalManualSetupStep/AddDeviceModalManualSetupStep.tsx +++ b/web/src/shared/components/modals/AddUserDeviceModal/steps/AddDeviceModalManualSetupStep/AddDeviceModalManualSetupStep.tsx @@ -7,8 +7,10 @@ import { formChangeLogic } from '../../../../../formLogic'; import './style.scss'; import { useStore } from '@tanstack/react-form'; import { useMutation } from '@tanstack/react-query'; +import type { AxiosError } from 'axios'; import { useMemo } from 'react'; import api from '../../../../../api/api'; +import type { ApiError } from '../../../../../api/types'; import { SizedBox } from '../../../../../defguard-ui/components/SizedBox/SizedBox'; import { ThemeSpacing } from '../../../../../defguard-ui/types'; import { patternValidWireguardKey } from '../../../../../patterns'; @@ -79,7 +81,7 @@ export const AddDeviceModalManualSetupStep = () => { onSubmit: formSchema, onChange: formSchema, }, - onSubmit: async ({ value }) => { + onSubmit: async ({ value, formApi }) => { let publicKey: string; let privateKey: string | undefined; @@ -95,8 +97,21 @@ export const AddDeviceModalManualSetupStep = () => { name: value.name, username, wireguard_pubkey: publicKey, + }).catch((e: AxiosError) => { + const msg = e.response?.data.msg; + if (msg?.includes('already exists')) { + formApi.setErrorMap({ + onSubmit: { + fields: { + publicKey: m.form_error_key_exists(), + }, + }, + }); + } }); + if (!createResponse) return; + if (!createResponse.data.configs.length) { useAddUserDeviceModal.getState().close(); } diff --git a/web/src/shared/components/modals/EditUserDeviceModal/EditUserDeviceModal.tsx b/web/src/shared/components/modals/EditUserDeviceModal/EditUserDeviceModal.tsx index 94e1fe9901..2b475e98ec 100644 --- a/web/src/shared/components/modals/EditUserDeviceModal/EditUserDeviceModal.tsx +++ b/web/src/shared/components/modals/EditUserDeviceModal/EditUserDeviceModal.tsx @@ -53,7 +53,7 @@ export const EditUserDeviceModal = () => { ); }; -const getFormSchema = (names: string[]) => +const getFormSchema = (names: string[], pubkeys: string[]) => z.object({ name: z .string() @@ -63,13 +63,23 @@ const getFormSchema = (names: string[]) => publicKey: z .string() .length(44, m.form_error_invalid()) - .regex(patternValidWireguardKey, m.form_error_invalid()), + .regex(patternValidWireguardKey, m.form_error_invalid()) + .refine((val) => !pubkeys.includes(val), m.form_error_key_exists()), }); -const ModalContent = ({ device, reservedNames, username }: OpenEditDeviceModal) => { +const ModalContent = ({ + device, + reservedNames, + reservedPubkeys, + username, +}: OpenEditDeviceModal) => { const formSchema = useMemo( - () => getFormSchema(reservedNames.filter((name) => name !== device.name)), - [reservedNames, device.name], + () => + getFormSchema( + reservedNames.filter((name) => name !== device.name), + reservedPubkeys.filter((key) => key !== device.wireguard_pubkey), + ), + [reservedNames, reservedPubkeys, device.name, device.wireguard_pubkey], ); const { mutateAsync } = useMutation({ diff --git a/web/src/shared/hooks/modalControls/types.ts b/web/src/shared/hooks/modalControls/types.ts index beadff12f5..cf9f1f860d 100644 --- a/web/src/shared/hooks/modalControls/types.ts +++ b/web/src/shared/hooks/modalControls/types.ts @@ -32,6 +32,7 @@ export interface OpenConfirmActionModal { export interface OpenEditDeviceModal { device: Device; reservedNames: string[]; + reservedPubkeys: string[]; username: string; } @@ -39,6 +40,7 @@ export interface OpenAuthKeyRenameModal { id: number; name: string; username: string; + reservedNames: string[]; } export interface OpenAddApiTokenModal {