fix(settings): improve error handling and success notifications in EvoBGP settings
Enhanced the error handling in the settings page by introducing a dedicated error message function. Added success and error toast notifications for better user feedback during settings save operations. Updated API key normalization to ensure consistent handling across the application.
This commit is contained in:
@@ -875,8 +875,11 @@ export default function SettingsPage() {
|
|||||||
await evo.saveSettings(patch)
|
await evo.saveSettings(patch)
|
||||||
setEvoKeyDraft("")
|
setEvoKeyDraft("")
|
||||||
markSaved()
|
markSaved()
|
||||||
|
toast.success("Настройки EvoBGP сохранены")
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setEvoSaveErr(e instanceof Error ? e.message : "Ошибка сохранения")
|
const msg = e instanceof Error ? e.message : "Ошибка сохранения"
|
||||||
|
setEvoSaveErr(msg)
|
||||||
|
toast.error(msg)
|
||||||
} finally {
|
} finally {
|
||||||
setEvoSaveBusy(false)
|
setEvoSaveBusy(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,12 @@ function normalizeBaseUrl(raw: string): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Сырой API-ключ без префикса Bearer (иначе EvoBGP получит `Bearer Bearer …`). */
|
||||||
|
function normalizeApiKey(raw: string): string {
|
||||||
|
const trimmed = raw.trim()
|
||||||
|
return trimmed.replace(/^Bearer\s+/i, "").trim()
|
||||||
|
}
|
||||||
|
|
||||||
interface EvoCatalogRaw {
|
interface EvoCatalogRaw {
|
||||||
modules: { items: Array<{ id: string; name: string; type: string }> }
|
modules: { items: Array<{ id: string; name: string; type: string }> }
|
||||||
domains: {
|
domains: {
|
||||||
@@ -158,7 +164,7 @@ async function fetchEvoJson<T>(root: string, path: string, token: string): Promi
|
|||||||
function credentialsFromDb(): { root: string; apiKey: string } | null {
|
function credentialsFromDb(): { root: string; apiKey: string } | null {
|
||||||
const row = ensureEvobgpRow()
|
const row = ensureEvobgpRow()
|
||||||
const root = normalizeBaseUrl(row.baseUrl)
|
const root = normalizeBaseUrl(row.baseUrl)
|
||||||
const apiKey = row.apiKey.trim()
|
const apiKey = normalizeApiKey(row.apiKey)
|
||||||
if (!root || !apiKey) return null
|
if (!root || !apiKey) return null
|
||||||
return { root, apiKey }
|
return { root, apiKey }
|
||||||
}
|
}
|
||||||
@@ -168,8 +174,8 @@ const evobgpRoutes: FastifyPluginAsyncZod = async (app) => {
|
|||||||
const row = ensureEvobgpRow()
|
const row = ensureEvobgpRow()
|
||||||
return reply.send({
|
return reply.send({
|
||||||
baseUrl: row.baseUrl ?? "",
|
baseUrl: row.baseUrl ?? "",
|
||||||
enabled: row.enabled ?? false,
|
enabled: Boolean(row.enabled),
|
||||||
secretConfigured: Boolean(row.apiKey?.trim()),
|
secretConfigured: Boolean(normalizeApiKey(row.apiKey ?? "")),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -183,10 +189,15 @@ const evobgpRoutes: FastifyPluginAsyncZod = async (app) => {
|
|||||||
let nextEnabled = cur.enabled
|
let nextEnabled = cur.enabled
|
||||||
let nextKey = cur.apiKey
|
let nextKey = cur.apiKey
|
||||||
|
|
||||||
if (parsed.data.baseUrl !== undefined) nextBase = parsed.data.baseUrl.trim()
|
if (parsed.data.baseUrl !== undefined) {
|
||||||
|
nextBase = normalizeBaseUrl(parsed.data.baseUrl)
|
||||||
|
}
|
||||||
if (parsed.data.enabled !== undefined) nextEnabled = parsed.data.enabled
|
if (parsed.data.enabled !== undefined) nextEnabled = parsed.data.enabled
|
||||||
if (parsed.data.apiKey !== undefined) {
|
if (parsed.data.apiKey !== undefined) {
|
||||||
nextKey = parsed.data.apiKey === null || parsed.data.apiKey === "" ? "" : parsed.data.apiKey.trim()
|
nextKey =
|
||||||
|
parsed.data.apiKey === null || parsed.data.apiKey === ""
|
||||||
|
? ""
|
||||||
|
: normalizeApiKey(parsed.data.apiKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
db.update(evobgpSettings)
|
db.update(evobgpSettings)
|
||||||
@@ -202,8 +213,8 @@ const evobgpRoutes: FastifyPluginAsyncZod = async (app) => {
|
|||||||
const row = ensureEvobgpRow()
|
const row = ensureEvobgpRow()
|
||||||
return reply.send({
|
return reply.send({
|
||||||
baseUrl: row.baseUrl ?? "",
|
baseUrl: row.baseUrl ?? "",
|
||||||
enabled: row.enabled ?? false,
|
enabled: Boolean(row.enabled),
|
||||||
secretConfigured: Boolean(row.apiKey?.trim()),
|
secretConfigured: Boolean(normalizeApiKey(row.apiKey ?? "")),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -223,7 +234,7 @@ const evobgpRoutes: FastifyPluginAsyncZod = async (app) => {
|
|||||||
const keyRaw =
|
const keyRaw =
|
||||||
d.apiKey !== undefined && d.apiKey.trim() !== "" ? d.apiKey : row.apiKey
|
d.apiKey !== undefined && d.apiKey.trim() !== "" ? d.apiKey : row.apiKey
|
||||||
const root = normalizeBaseUrl(urlRaw.trim())
|
const root = normalizeBaseUrl(urlRaw.trim())
|
||||||
const token = keyRaw.trim()
|
const token = normalizeApiKey(keyRaw)
|
||||||
if (!root || !token) {
|
if (!root || !token) {
|
||||||
return reply.status(400).send({
|
return reply.status(400).send({
|
||||||
error: "Нужны базовый URL и API-ключ (в форме или уже сохранённые в БД)",
|
error: "Нужны базовый URL и API-ключ (в форме или уже сохранённые в БД)",
|
||||||
|
|||||||
+56
-57
@@ -10,6 +10,7 @@ import {
|
|||||||
} from "react"
|
} from "react"
|
||||||
import { useDataSource } from "@/lib/data-source"
|
import { useDataSource } from "@/lib/data-source"
|
||||||
import type { Domain, IpRange, Asn } from "@/lib/data"
|
import type { Domain, IpRange, Asn } from "@/lib/data"
|
||||||
|
import { ApiClientError, requestJson } from "@/shared/api/http-client"
|
||||||
|
|
||||||
export interface EvoBgpCommunityRow {
|
export interface EvoBgpCommunityRow {
|
||||||
id: string
|
id: string
|
||||||
@@ -66,6 +67,12 @@ interface EvoBgpContextValue {
|
|||||||
|
|
||||||
const EvoBgpContext = createContext<EvoBgpContextValue | null>(null)
|
const EvoBgpContext = createContext<EvoBgpContextValue | null>(null)
|
||||||
|
|
||||||
|
function errorMessage(e: unknown, fallback: string): string {
|
||||||
|
if (e instanceof ApiClientError) return e.message || fallback
|
||||||
|
if (e instanceof Error) return e.message || fallback
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
export function EvoBGPProvider({ children }: { children: React.ReactNode }) {
|
export function EvoBGPProvider({ children }: { children: React.ReactNode }) {
|
||||||
const { mode, backendUrl, backendStatus } = useDataSource()
|
const { mode, backendUrl, backendStatus } = useDataSource()
|
||||||
const [baseUrl, setBaseUrlState] = useState("")
|
const [baseUrl, setBaseUrlState] = useState("")
|
||||||
@@ -86,24 +93,15 @@ export function EvoBGPProvider({ children }: { children: React.ReactNode }) {
|
|||||||
setLoading(true)
|
setLoading(true)
|
||||||
setError(null)
|
setError(null)
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${backendUrl.replace(/\/$/, "")}/api/evobgp/catalog`, {
|
const data = await requestJson<EvoBgpCatalogSnapshot>(
|
||||||
method: "POST",
|
backendUrl,
|
||||||
})
|
"/api/evobgp/catalog",
|
||||||
const text = await res.text()
|
{ method: "POST" },
|
||||||
if (!res.ok) {
|
)
|
||||||
let msg = res.statusText
|
setSnapshot(data)
|
||||||
try {
|
|
||||||
const j = JSON.parse(text) as { error?: string; detail?: string }
|
|
||||||
msg = j.error ?? j.detail ?? msg
|
|
||||||
} catch {
|
|
||||||
if (text) msg = text
|
|
||||||
}
|
|
||||||
throw new Error(msg || "Ошибка EvoBGP")
|
|
||||||
}
|
|
||||||
setSnapshot(JSON.parse(text) as EvoBgpCatalogSnapshot)
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setSnapshot(null)
|
setSnapshot(null)
|
||||||
setError(e instanceof Error ? e.message : "Ошибка загрузки")
|
setError(errorMessage(e, "Ошибка загрузки"))
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
@@ -119,16 +117,19 @@ export function EvoBGPProvider({ children }: { children: React.ReactNode }) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${backendUrl.replace(/\/$/, "")}/api/evobgp/settings`)
|
const data = await requestJson<EvoBgpSettingsDto>(
|
||||||
if (!res.ok) throw new Error(await res.text())
|
backendUrl,
|
||||||
const data = (await res.json()) as EvoBgpSettingsDto
|
"/api/evobgp/settings",
|
||||||
|
)
|
||||||
setBaseUrlState(data.baseUrl ?? "")
|
setBaseUrlState(data.baseUrl ?? "")
|
||||||
setEnabledState(data.enabled ?? false)
|
setEnabledState(Boolean(data.enabled))
|
||||||
setSecretConfigured(data.secretConfigured ?? false)
|
setSecretConfigured(Boolean(data.secretConfigured))
|
||||||
setSettingsLoaded(true)
|
setSettingsLoaded(true)
|
||||||
await pullCatalog(data.enabled ?? false)
|
setError(null)
|
||||||
} catch {
|
await pullCatalog(Boolean(data.enabled))
|
||||||
|
} catch (e) {
|
||||||
setSettingsLoaded(true)
|
setSettingsLoaded(true)
|
||||||
|
setError(errorMessage(e, "Не удалось загрузить настройки EvoBGP"))
|
||||||
}
|
}
|
||||||
}, [mode, backendStatus, backendUrl, pullCatalog])
|
}, [mode, backendStatus, backendUrl, pullCatalog])
|
||||||
|
|
||||||
@@ -140,27 +141,25 @@ export function EvoBGPProvider({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
const saveSettings = useCallback(
|
const saveSettings = useCallback(
|
||||||
async (patch: EvoBgpSavePayload) => {
|
async (patch: EvoBgpSavePayload) => {
|
||||||
const res = await fetch(`${backendUrl.replace(/\/$/, "")}/api/evobgp/settings`, {
|
const data = await requestJson<EvoBgpSettingsDto>(
|
||||||
method: "PUT",
|
backendUrl,
|
||||||
headers: { "Content-Type": "application/json" },
|
"/api/evobgp/settings",
|
||||||
body: JSON.stringify(patch),
|
{
|
||||||
})
|
method: "PUT",
|
||||||
const text = await res.text()
|
body: JSON.stringify(patch),
|
||||||
if (!res.ok) {
|
},
|
||||||
let msg = res.statusText
|
)
|
||||||
try {
|
const nextEnabled = Boolean(data.enabled)
|
||||||
const j = JSON.parse(text) as { error?: string }
|
|
||||||
msg = j.error ?? msg
|
|
||||||
} catch {
|
|
||||||
if (text) msg = text
|
|
||||||
}
|
|
||||||
throw new Error(msg || "Не удалось сохранить")
|
|
||||||
}
|
|
||||||
const data = JSON.parse(text) as EvoBgpSettingsDto
|
|
||||||
setBaseUrlState(data.baseUrl ?? "")
|
setBaseUrlState(data.baseUrl ?? "")
|
||||||
setEnabledState(data.enabled ?? false)
|
setEnabledState(nextEnabled)
|
||||||
setSecretConfigured(data.secretConfigured ?? false)
|
setSecretConfigured(Boolean(data.secretConfigured))
|
||||||
await pullCatalog(data.enabled ?? false)
|
setError(null)
|
||||||
|
// Каталог не должен ронять успех сохранения (401/502 на catalog ≠ «настройки не сохранились»)
|
||||||
|
try {
|
||||||
|
await pullCatalog(nextEnabled)
|
||||||
|
} catch {
|
||||||
|
/* pullCatalog already sets error state */
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[backendUrl, pullCatalog],
|
[backendUrl, pullCatalog],
|
||||||
)
|
)
|
||||||
@@ -169,20 +168,20 @@ export function EvoBGPProvider({ children }: { children: React.ReactNode }) {
|
|||||||
await pullCatalog(enabled)
|
await pullCatalog(enabled)
|
||||||
}, [enabled, pullCatalog])
|
}, [enabled, pullCatalog])
|
||||||
|
|
||||||
const testConnection = useCallback(async (draft?: EvoBgpTestDraft) => {
|
const testConnection = useCallback(
|
||||||
try {
|
async (draft?: EvoBgpTestDraft) => {
|
||||||
const res = await fetch(`${backendUrl.replace(/\/$/, "")}/api/evobgp/test`, {
|
try {
|
||||||
method: "POST",
|
await requestJson<{ ok?: boolean }>(backendUrl, "/api/evobgp/test", {
|
||||||
headers: { "Content-Type": "application/json" },
|
method: "POST",
|
||||||
body: JSON.stringify(draft ?? {}),
|
body: JSON.stringify(draft ?? {}),
|
||||||
})
|
})
|
||||||
const data = (await res.json().catch(() => ({}))) as { ok?: boolean; error?: string }
|
return { ok: true, message: "Соединение с EvoBGP установлено" }
|
||||||
if (!res.ok) throw new Error(data.error ?? res.statusText)
|
} catch (e) {
|
||||||
return { ok: true, message: "Соединение с EvoBGP установлено" }
|
return { ok: false, message: errorMessage(e, "Ошибка") }
|
||||||
} catch (e) {
|
}
|
||||||
return { ok: false, message: e instanceof Error ? e.message : "Ошибка" }
|
},
|
||||||
}
|
[backendUrl],
|
||||||
}, [backendUrl])
|
)
|
||||||
|
|
||||||
const value = useMemo(
|
const value = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
|
|||||||
Reference in New Issue
Block a user