diff --git a/new-ui/src/pages/full/AddInstancePage/AddInstancePage.tsx b/new-ui/src/pages/full/AddInstancePage/AddInstancePage.tsx index b1680db4..2ee2f08b 100644 --- a/new-ui/src/pages/full/AddInstancePage/AddInstancePage.tsx +++ b/new-ui/src/pages/full/AddInstancePage/AddInstancePage.tsx @@ -12,6 +12,7 @@ import { edgeApi } from '../../../shared/edge-api/api'; import { useAppForm } from '../../../shared/form'; import { formChangeLogic } from '../../../shared/formLogic'; import { FullPage } from '../../../shared/layouts/FullPage/FullPage'; +import { Snackbar } from '../../../shared/providers/snackbar/snackbar'; import { ThemeSpacing } from '../../../shared/types'; import { isPresent } from '../../../shared/utils/isPresent'; import { useEnrollmentStore } from '../EnrollmentPage/hooks/useEnrollmentStore'; @@ -48,26 +49,26 @@ export const AddInstancePage = () => { }, onSubmit: async ({ value, formApi }) => { const result = await edgeApi.addInstance(value); - if (result.error) { - if (result.error.toLowerCase().includes('device name')) { + if (result.error ?? result.errorKind) { + if (result.errorKind === 'network') { formApi.setErrorMap({ - onSubmit: { - fields: { - name: 'Name already used.', - }, - }, + onSubmit: { fields: { url: 'Invalid URL.' } }, }); return; - } else { + } + if (result.errorKind === 'unauthorized') { + formApi.setErrorMap({ + onSubmit: { fields: { token: 'Invalid token.' } }, + }); + return; + } + if (result.error?.toLowerCase().includes('device name')) { formApi.setErrorMap({ - onSubmit: { - fields: { - token: 'Invalid Token or URL', - url: 'Invalid Token or URL', - }, - }, + onSubmit: { fields: { name: 'Name already used.' } }, }); + return; } + Snackbar.error('Communication error, contact administrator.'); return; } if (result.cookie) { diff --git a/new-ui/src/pages/full/OverviewPage/components/UpdateInstanceModal/UpdateInstanceModal.tsx b/new-ui/src/pages/full/OverviewPage/components/UpdateInstanceModal/UpdateInstanceModal.tsx index 5808b46e..598035fe 100644 --- a/new-ui/src/pages/full/OverviewPage/components/UpdateInstanceModal/UpdateInstanceModal.tsx +++ b/new-ui/src/pages/full/OverviewPage/components/UpdateInstanceModal/UpdateInstanceModal.tsx @@ -80,19 +80,20 @@ const ModalContent = ({ data }: { data: OpenUpdateInstanceModalData }) => { url: value.url, token: value.token, }); - if (result.error) { - if (result.isCredentialsError) { + if (result.error ?? result.errorKind) { + if (result.errorKind === 'network') { formApi.setErrorMap({ - onSubmit: { - fields: { - token: 'Invalid Token or URL', - url: 'Invalid Token or URL', - }, - }, + onSubmit: { fields: { url: 'Invalid URL.' } }, }); - } else { - Snackbar.error(result.error); + return; } + if (result.errorKind === 'unauthorized') { + formApi.setErrorMap({ + onSubmit: { fields: { token: 'Invalid token.' } }, + }); + return; + } + Snackbar.error('Communication error, contact administrator.'); return; } Snackbar.default('Instance updated.'); diff --git a/new-ui/src/shared/edge-api/api.ts b/new-ui/src/shared/edge-api/api.ts index 757013ec..b9d4769d 100644 --- a/new-ui/src/shared/edge-api/api.ts +++ b/new-ui/src/shared/edge-api/api.ts @@ -16,6 +16,7 @@ import type { AddInstanceRequest, AddInstanceResult, EdgeRequestHeaders, + EnrollmentErrorKind, EnrollmentStartResponse, MfaSetupFinishRequest, MfaSetupFinishResponse, @@ -78,22 +79,52 @@ const createDevice = async ( return {}; }; +type EnrollmentStartOutcome = + | { ok: true; response: Response } + | { ok: false; error?: string; errorKind: EnrollmentErrorKind }; + +const startEnrollment = async ( + proxyUrl: string, + token: string, + edgeHeaders: EdgeRequestHeaders, +): Promise => { + let res: Response; + try { + res = await fetch(`${proxyUrl}/enrollment/start`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...edgeHeaders }, + body: JSON.stringify({ token }), + }); + } catch { + return { ok: false, errorKind: 'network' }; + } + + if (!res.ok) { + if (res.status === 401) { + return { ok: false, errorKind: 'unauthorized' }; + } + const body = (await res.json().catch(() => ({}))) as { error?: string }; + return { + ok: false, + error: body.error ?? `Enrollment start failed (${res.status})`, + errorKind: 'server', + }; + } + + return { ok: true, response: res }; +}; + const addInstance = async (values: AddInstanceRequest): Promise => { try { const proxyUrl = buildProxyUrl(values.url); const edgeHeaders = await getEdgeRequestHeaders(); - const startRes = await fetch(`${proxyUrl}/enrollment/start`, { - method: 'POST', - headers: { 'Content-Type': 'application/json', ...edgeHeaders }, - body: JSON.stringify({ token: values.token }), - }); - - if (!startRes.ok) { - const body = (await startRes.json()) as { error?: string }; - return { error: body.error ?? `Enrollment start failed (${startRes.status})` }; + const startResult = await startEnrollment(proxyUrl, values.token, edgeHeaders); + if (!startResult.ok) { + return { error: startResult.error, errorKind: startResult.errorKind }; } + const startRes = startResult.response; const cookie = startRes.headers .getSetCookie() @@ -149,19 +180,11 @@ const updateExistingInstance = async ( const existing = instances.find((i) => i.id === values.instanceId); if (!existing) return { error: 'Instance no longer exists.' }; - const startRes = await fetch(`${proxyUrl}/enrollment/start`, { - method: 'POST', - headers: { 'Content-Type': 'application/json', ...edgeHeaders }, - body: JSON.stringify({ token: values.token }), - }); - - if (!startRes.ok) { - const body = (await startRes.json()) as { error?: string }; - return { - error: body.error ?? `Enrollment start failed (${startRes.status})`, - isCredentialsError: true, - }; + const startResult = await startEnrollment(proxyUrl, values.token, edgeHeaders); + if (!startResult.ok) { + return { error: startResult.error, errorKind: startResult.errorKind }; } + const startRes = startResult.response; const cookie = startRes.headers .getSetCookie() @@ -173,7 +196,7 @@ const updateExistingInstance = async ( if (resp.instance.id !== existing.uuid) { return { error: 'Provided token belongs to a different instance.', - isCredentialsError: true, + errorKind: 'unauthorized', }; } diff --git a/new-ui/src/shared/edge-api/types.ts b/new-ui/src/shared/edge-api/types.ts index ce7611bc..26eb60df 100644 --- a/new-ui/src/shared/edge-api/types.ts +++ b/new-ui/src/shared/edge-api/types.ts @@ -49,19 +49,24 @@ export type EdgeRequestHeaders = { 'defguard-client-platform': string; }; +/** `network`: the request could not be sent, most likely a bad URL. + * `unauthorized`: the server responded 401, the token is invalid. + * `server`: any other failure response. */ +export type EnrollmentErrorKind = 'network' | 'unauthorized' | 'server'; + export type AddInstanceRequest = { url: string; token: string; name: string }; export type AddInstanceResult = { startResponse?: EnrollmentStartResponse; proxyUrl?: string; cookie?: string; error?: string; + errorKind?: EnrollmentErrorKind; }; export type UpdateInstanceRequest = { instanceId: number; url: string; token: string }; export type UpdateInstanceResult = { error?: string; - /** Set when the error is about the provided url/token and should be shown on the form fields. */ - isCredentialsError?: boolean; + errorKind?: EnrollmentErrorKind; }; export type MfaSetupStartRequest = { method: MfaMethodValue };