diff --git a/codex-storage-marketplace-ui-components-0.0.51.tgz b/codex-storage-marketplace-ui-components-0.0.51.tgz new file mode 100644 index 0000000..bdb8b85 Binary files /dev/null and b/codex-storage-marketplace-ui-components-0.0.51.tgz differ diff --git a/codex-storage-sdk-js-0.1.1.tgz b/codex-storage-sdk-js-0.1.1.tgz new file mode 100644 index 0000000..d54c150 Binary files /dev/null and b/codex-storage-sdk-js-0.1.1.tgz differ diff --git a/e2e/onboarding.spec.ts b/e2e/onboarding.spec.ts index 6f362f5..baee755 100644 --- a/e2e/onboarding.spec.ts +++ b/e2e/onboarding.spec.ts @@ -77,6 +77,7 @@ test("does not display undefined when delete the url value", async ({ } await expect(page.locator("#url")).toHaveValue(""); + await page.locator("#url").blur(); await expect(page.locator("#url")).toHaveAttribute("aria-invalid"); await expect(page.locator(".refresh svg")).toHaveAttribute( "color", diff --git a/e2e/settings.spec.ts b/e2e/settings.spec.ts index 94d8192..288feab 100644 --- a/e2e/settings.spec.ts +++ b/e2e/settings.spec.ts @@ -8,23 +8,80 @@ import test, { expect } from "@playwright/test"; // await expect(page.locator('span').filter({ hasText: 'success ! The log level has' }).locator('b')).toBeVisible(); // }) -test('update the URL with wrong URL applies', async ({ page }) => { - await page.goto('/dashboard'); - await page.locator('a').filter({ hasText: 'Settings' }).click(); - await page.getByLabel('Address').click(); - await page.getByLabel('Address').fill('hello'); - await expect(page.getByLabel('Address')).toHaveAttribute("aria-invalid") - await expect(page.locator(".refresh svg")).toHaveAttribute("color", "#494949") - await page.getByLabel('Address').fill('http://127.0.0.1:8079'); - await expect(page.getByLabel('Address')).not.toHaveAttribute("aria-invalid") - await expect(page.locator(".refresh svg")).not.toHaveAttribute("aria-disabled") - await expect(page.getByLabel('Address')).toHaveValue("http://127.0.0.1") - await expect(page.getByLabel('Port')).toHaveValue("8079") - await page.locator(".refresh").click() - await expect(page.locator(".health-checks ul li").nth(2).getByTestId("icon-error")).toBeVisible() - await expect(page.locator(".health-checks ul li").nth(2).getByTestId("icon-success")).not.toBeVisible() - await page.getByLabel('Address').fill('http://127.0.0.1:8080'); - await page.locator(".refresh").click() - await expect(page.locator(".health-checks ul li").nth(2).getByTestId("icon-error")).not.toBeVisible() - await expect(page.locator(".health-checks ul li").nth(2).getByTestId("icon-success")).toBeVisible() -}) \ No newline at end of file +test("update the URL with wrong URL applies", async ({ page }) => { + await page.goto("/dashboard"); + await page.locator("a").filter({ hasText: "Settings" }).click(); + await page.getByLabel("Address").click(); + await page.getByLabel("Address").fill("hello"); + await page.getByLabel("Address").blur(); + await expect(page.getByLabel("Address")).toHaveAttribute("aria-invalid"); + await expect(page.locator(".refresh svg")).toHaveAttribute( + "color", + "#494949" + ); + await page.getByLabel("Address").fill("http://127.0.0.1:8079"); + await page.getByLabel("Address").blur(); + await expect(page.getByLabel("Address")).not.toHaveAttribute("aria-invalid"); + await expect(page.locator(".refresh svg")).not.toHaveAttribute( + "aria-disabled" + ); + await expect(page.getByLabel("Address")).toHaveValue("http://127.0.0.1"); + await expect(page.getByLabel("Port")).toHaveValue("8079"); + await page.locator(".refresh").click(); + await expect( + page.locator(".health-checks ul li").nth(2).getByTestId("icon-error") + ).toBeVisible(); + await expect( + page.locator(".health-checks ul li").nth(2).getByTestId("icon-success") + ).not.toBeVisible(); + await page.getByLabel("Address").fill("http://127.0.0.1:8080"); + await page.locator(".refresh").click(); + await expect( + page.locator(".health-checks ul li").nth(2).getByTestId("icon-error") + ).not.toBeVisible(); + await expect( + page.locator(".health-checks ul li").nth(2).getByTestId("icon-success") + ).toBeVisible(); +}); + +test("update the URL with basic auth save the credentials", async ({ + page, +}) => { + await page.goto("/dashboard"); + await page.locator("a").filter({ hasText: "Settings" }).click(); + await page.getByLabel("Address").click(); + await page.getByLabel("Address").fill("http://hello:world@localhost:8080"); + await page.getByLabel("Address").blur(); + await expect(page.getByLabel("Address")).not.toHaveAttribute("aria-invalid"); + await expect(page.locator(".refresh svg")).not.toHaveAttribute( + "aria-disabled" + ); + + let failedRequestAuthorization; + page.on("requestfailed", async (request) => { + failedRequestAuthorization = await request.headerValue("Authorization"); + }); + + await page.locator(".refresh").click(); + + await page.waitForTimeout(1000); + + expect(failedRequestAuthorization).toBe("Basic " + btoa("hello:world")); + + await page.getByLabel("Address").click(); + await page.getByLabel("Address").fill("http://localhost:8080"); + await page.getByLabel("Address").blur(); + + let successRequestAuthorization: string | null = "failed"; + page.on("request", async (request) => { + if (request.url().endsWith("/api/codex/v1/spr")) { + successRequestAuthorization = await request.headerValue("Authorization"); + } + }); + + await page.locator(".refresh").click(); + + await page.waitForRequest("http://localhost:8080/api/codex/v1/spr"); + + expect(successRequestAuthorization).toBeNull(); +}); diff --git a/package-lock.json b/package-lock.json index 15a72bd..e3b9341 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,8 @@ "version": "0.0.16", "license": "MIT", "dependencies": { - "@codex-storage/marketplace-ui-components": "^0.0.51", - "@codex-storage/sdk-js": "^0.0.23", + "@codex-storage/marketplace-ui-components": "./codex-storage-marketplace-ui-components-0.0.51.tgz", + "@codex-storage/sdk-js": "./codex-storage-sdk-js-0.1.1.tgz", "@sentry/browser": "^8.32.0", "@sentry/react": "^8.31.0", "@tanstack/react-query": "^5.51.15", @@ -420,8 +420,8 @@ }, "node_modules/@codex-storage/marketplace-ui-components": { "version": "0.0.51", - "resolved": "https://registry.npmjs.org/@codex-storage/marketplace-ui-components/-/marketplace-ui-components-0.0.51.tgz", - "integrity": "sha512-KPPFlcpx3a83WCBSLbRONrF/yr4J/ctyTfFPxMaRSMTRD1LtfIE0uPy3QxtHs6tigOts2h4DEz6Kn2ynHdfKPg==", + "resolved": "file:codex-storage-marketplace-ui-components-0.0.51.tgz", + "integrity": "sha512-rIgI20HA4CbccaxJPVZJw+QvoxDpY6g2FWX1HDChFqYqBcw+AP1h2gtVSau5cSZn6Zn/EQIgMlYZk+1m0U3utQ==", "license": "MIT", "engines": { "node": ">=18" @@ -433,15 +433,16 @@ } }, "node_modules/@codex-storage/sdk-js": { - "version": "0.0.23", - "resolved": "https://registry.npmjs.org/@codex-storage/sdk-js/-/sdk-js-0.0.23.tgz", - "integrity": "sha512-+ktJs396GERPudRh5zjTvOMjwo3mRHVYN901Qvs0q3YlRK983aewSqJ+Z7NttSQ27oxTcvxQVrilcvzZRHQTkg==", + "version": "0.1.1", + "resolved": "file:codex-storage-sdk-js-0.1.1.tgz", + "integrity": "sha512-BwCYirG0gwNJxxF0c3Zkrachi9FcYH6sLGUE9V07zIPaC0XVtiJiyUXhP0ciI28jgg2sFqk5nim/5ZmMaQmqBw==", "license": "MIT", "dependencies": { + "undici": "^7.5.0", "valibot": "^0.32.0" }, "engines": { - "node": ">=20" + "node": ">=20.18.1" } }, "node_modules/@csstools/selector-resolve-nested": { @@ -5173,6 +5174,15 @@ "node": ">=14.17" } }, + "node_modules/undici": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.6.0.tgz", + "integrity": "sha512-gaFsbThjrDGvAaD670r81RZro/s6H2PVZF640Qn0p5kZK+/rim7/mmyfp2W7VB5vOMaFM8vuFBJUaMlaZTYHlA==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { "version": "6.20.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", diff --git a/package.json b/package.json index ef11fe8..9c5665f 100644 --- a/package.json +++ b/package.json @@ -26,8 +26,8 @@ "React" ], "dependencies": { - "@codex-storage/marketplace-ui-components": "^0.0.51", - "@codex-storage/sdk-js": "^0.0.23", + "@codex-storage/marketplace-ui-components": "./codex-storage-marketplace-ui-components-0.0.51.tgz", + "@codex-storage/sdk-js": "./codex-storage-sdk-js-0.1.1.tgz", "@sentry/browser": "^8.32.0", "@sentry/react": "^8.31.0", "@tanstack/react-query": "^5.51.15", diff --git a/src/components/CodexUrllSettings/CodexUrlSettings.tsx b/src/components/CodexUrllSettings/CodexUrlSettings.tsx deleted file mode 100644 index 31bfe5d..0000000 --- a/src/components/CodexUrllSettings/CodexUrlSettings.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { useState } from "react"; -import { Button, Input, Toast } from "@codex-storage/marketplace-ui-components"; -import { CodexSdk } from "../../sdk/codex"; - -export function CodexUrlSettings() { - const queryClient = useQueryClient(); - const [url, setUrl] = useState(CodexSdk.url); - const [isInvalid, setIsInvalid] = useState(false); - const [toast, setToast] = useState({ time: 0, message: "" }); - const { mutateAsync } = useMutation({ - mutationFn: (url: string) => CodexSdk.updateURL(url), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["spr"] }); - - setToast({ message: "Settings saved successfully.", time: Date.now() }); - }, - }); - - const onChange = (e: React.FormEvent) => { - const element = e.currentTarget; - const value = element.value; - - setUrl(value); - setIsInvalid(!element.checkValidity()); - }; - - const onClick = () => { - if (isInvalid === false) { - mutateAsync(url); - } - }; - - return ( - <> -
- -
- - - - ); -} diff --git a/src/components/HealthChecks/HealthChecks.tsx b/src/components/HealthChecks/HealthChecks.tsx index e9a1935..49bf1b1 100644 --- a/src/components/HealthChecks/HealthChecks.tsx +++ b/src/components/HealthChecks/HealthChecks.tsx @@ -31,6 +31,7 @@ export function HealthChecks({ online, onStepValid }: Props) { HealthCheckUtils.removePort(CodexSdk.url()) ); const [port, setPort] = useState(HealthCheckUtils.getPort(CodexSdk.url())); + const [auth, setAuth] = useState(""); const queryClient = useQueryClient(); useEffect( @@ -46,23 +47,31 @@ export function HealthChecks({ online, onStepValid }: Props) { [persistence.refetch, onStepValid, portForwarding.refetch, codex.isSuccess] ); - const onAddressChange = (e: React.FormEvent) => { + const onAddressBlur = (e: React.FormEvent) => { const element = e.currentTarget; const value = e.currentTarget.value; setIsAddressInvalid(!element.checkValidity()); - setAddress(value); + const { auth, url } = HealthCheckUtils.extractBasicAuth(value); - if (HealthCheckUtils.containsPort(value)) { - const address = HealthCheckUtils.removePort(value); + setAddress(url); + + if (HealthCheckUtils.containsPort(url)) { + const address = HealthCheckUtils.removePort(url); setAddress(address); - const p = HealthCheckUtils.getPort(value); + const p = HealthCheckUtils.getPort(url); setPort(p); } else { - setAddress(value); + setAddress(url); } + + setAuth(auth || ""); + }; + + const onChange = (e: React.FormEvent) => { + setAddress(e.currentTarget.value); }; const onPortChange = (e: React.FormEvent) => { @@ -70,19 +79,25 @@ export function HealthChecks({ online, onStepValid }: Props) { const value = element.value; setIsPortInvalid(!element.checkValidity()); - setPort(parseInt(value, 10)); + setPort(value); }; const onSave = () => { - const url = address + ":" + port; + let url = address; + + if (port) { + url += ":" + port; + } if (HealthCheckUtils.isUrlInvalid(url)) { return; } - CodexSdk.updateURL(url) + CodexSdk.updateURL(url, { auth: { basic: auth } }) .then(() => queryClient.invalidateQueries()) .then(() => codex.refetch()); + + setAuth(""); }; return ( @@ -98,9 +113,11 @@ export function HealthChecks({ online, onStepValid }: Props) { type="url" label="Address" required + mode={"manual"} isInvalid={isAddressInvalid} - onChange={onAddressChange} + onBlur={onAddressBlur} value={address} + onChange={onChange} placeholder="127.0.0.1"> {isAddressInvalid ? ( diff --git a/src/components/HealthChecks/health-check.utils.test.ts b/src/components/HealthChecks/health-check.utils.test.ts index 0d6e1e7..7cb07bc 100644 --- a/src/components/HealthChecks/health-check.utils.test.ts +++ b/src/components/HealthChecks/health-check.utils.test.ts @@ -2,155 +2,167 @@ import { assert, describe, it } from "vitest"; import { HealthCheckUtils } from "./health-check.utils"; describe("health check", () => { - it("remove the port from an url", async () => { - assert.deepEqual(HealthCheckUtils.removePort("http://localhost:8080"), "http://localhost"); - }); + it("remove the port from an url", async () => { + assert.deepEqual( + HealthCheckUtils.removePort("http://localhost:8080"), + "http://localhost" + ); + }); - it("get the port from an url", async () => { - assert.deepEqual(HealthCheckUtils.getPort("http://localhost:8080"), 8080); - }); + it("get the port from an url", async () => { + assert.deepEqual(HealthCheckUtils.getPort("http://localhost:8080"), "8080"); + }); - it("get the default port when the url does not contain the port", async () => { - assert.deepEqual(HealthCheckUtils.getPort("http://localhost"), 80); - }); + it("get the default port when the url does not contain the port", async () => { + assert.deepEqual(HealthCheckUtils.getPort("http://localhost"), ""); + }); - it("returns true when the url contains a port", async () => { - assert.deepEqual(HealthCheckUtils.containsPort("http://localhost:8080"), true); - }); + it("returns true when the url contains a port", async () => { + assert.deepEqual( + HealthCheckUtils.containsPort("http://localhost:8080"), + true + ); + }); - it("returns false when the url does not contain a port", async () => { - assert.deepEqual(HealthCheckUtils.containsPort("http://localhost"), false); - }); + it("returns false when the url does not contain a port", async () => { + assert.deepEqual(HealthCheckUtils.containsPort("http://localhost"), false); + }); + it("extracts the basic authentication", async () => { + assert.deepEqual( + HealthCheckUtils.extractBasicAuth("http://hello:world@localhost:8080"), + { auth: "hello:world", url: "http://localhost:8080" } + ); + }); - it("returns true when the url is invalid", async () => { - assert.deepEqual(HealthCheckUtils.isUrlInvalid("http://"), true); - }); + it("returns true when the url is invalid", async () => { + assert.deepEqual(HealthCheckUtils.isUrlInvalid("http://"), true); + }); - it("returns false when the url is valid", async () => { - assert.deepEqual(HealthCheckUtils.isUrlInvalid("http://localhost:8080"), false); - }); + it("returns false when the url is valid", async () => { + assert.deepEqual( + HealthCheckUtils.isUrlInvalid("http://localhost:8080"), + false + ); + }); - it("returns the tcp port", async () => { - const debug = { - "id": "a", - "addrs": [ - "/ip4/127.0.0.1/tcp/8070" - ], - "repo": "", - "spr": "", - "announceAddresses": [ - "/ip4/127.0.0.1/tcp/8070" - ], - "table": { - "localNode": { - "nodeId": "", - "peerId": "", - "record": "", - "address": "0.0.0.0:8090", - "seen": false - }, - "nodes": [] - }, - "codex": { - "version": "v0.1.0\nv0.1.1\nv0.1.2\nv0.1.3\nv0.1.4\nv0.1.5\nv0.1.6\nv0.1.7", - "revision": "2fb7031e" - } - } - assert.deepEqual(HealthCheckUtils.getTcpPort(debug), { error: false, data: 8070 }); + it("returns the tcp port", async () => { + const debug = { + id: "a", + addrs: ["/ip4/127.0.0.1/tcp/8070"], + repo: "", + spr: "", + announceAddresses: ["/ip4/127.0.0.1/tcp/8070"], + table: { + localNode: { + nodeId: "", + peerId: "", + record: "", + address: "0.0.0.0:8090", + seen: false, + }, + nodes: [], + }, + codex: { + version: + "v0.1.0\nv0.1.1\nv0.1.2\nv0.1.3\nv0.1.4\nv0.1.5\nv0.1.6\nv0.1.7", + revision: "2fb7031e", + }, + }; + assert.deepEqual(HealthCheckUtils.getTcpPort(debug), { + error: false, + data: 8070, }); + }); - it("returns an error when the addr is empty", async () => { - const debug = { - "id": "a", - "addrs": [ - ], - "repo": "", - "spr": "", - "announceAddresses": [ - "/ip4/127.0.0.1/tcp/8070" - ], - "table": { - "localNode": { - "nodeId": "", - "peerId": "", - "record": "", - "address": "0.0.0.0:8090", - "seen": false - }, - "nodes": [] - }, - "codex": { - "version": "v0.1.0\nv0.1.1\nv0.1.2\nv0.1.3\nv0.1.4\nv0.1.5\nv0.1.6\nv0.1.7", - "revision": "2fb7031e" - } - } - assert.deepEqual(HealthCheckUtils.getTcpPort(debug).error, true); - }); + it("returns an error when the addr is empty", async () => { + const debug = { + id: "a", + addrs: [], + repo: "", + spr: "", + announceAddresses: ["/ip4/127.0.0.1/tcp/8070"], + table: { + localNode: { + nodeId: "", + peerId: "", + record: "", + address: "0.0.0.0:8090", + seen: false, + }, + nodes: [], + }, + codex: { + version: + "v0.1.0\nv0.1.1\nv0.1.2\nv0.1.3\nv0.1.4\nv0.1.5\nv0.1.6\nv0.1.7", + revision: "2fb7031e", + }, + }; + assert.deepEqual(HealthCheckUtils.getTcpPort(debug).error, true); + }); - it("returns an error when the addr is misformated", async () => { - const debug = { - "id": "a", - "addrs": [ - "/ip4/127.0.0.1/tcp/hello" - ], - "repo": "", - "spr": "", - "announceAddresses": [ - "/ip4/127.0.0.1/tcp/8070" - ], - "table": { - "localNode": { - "nodeId": "", - "peerId": "", - "record": "", - "address": "0.0.0.0:8090", - "seen": false - }, - "nodes": [] - }, - "codex": { - "version": "v0.1.0\nv0.1.1\nv0.1.2\nv0.1.3\nv0.1.4\nv0.1.5\nv0.1.6\nv0.1.7", - "revision": "2fb7031e" - } - } - assert.deepEqual(HealthCheckUtils.getTcpPort(debug).error, true); - }); + it("returns an error when the addr is misformated", async () => { + const debug = { + id: "a", + addrs: ["/ip4/127.0.0.1/tcp/hello"], + repo: "", + spr: "", + announceAddresses: ["/ip4/127.0.0.1/tcp/8070"], + table: { + localNode: { + nodeId: "", + peerId: "", + record: "", + address: "0.0.0.0:8090", + seen: false, + }, + nodes: [], + }, + codex: { + version: + "v0.1.0\nv0.1.1\nv0.1.2\nv0.1.3\nv0.1.4\nv0.1.5\nv0.1.6\nv0.1.7", + revision: "2fb7031e", + }, + }; + assert.deepEqual(HealthCheckUtils.getTcpPort(debug).error, true); + }); - it("returns an error when the port is misformated", async () => { - const debug = { - "id": "a", - "addrs": [ - "hello" - ], - "repo": "", - "spr": "", - "announceAddresses": [ - "/ip4/127.0.0.1/tcp/8070" - ], - "table": { - "localNode": { - "nodeId": "", - "peerId": "", - "record": "", - "address": "0.0.0.0:8090", - "seen": false - }, - "nodes": [] - }, - "codex": { - "version": "v0.1.0\nv0.1.1\nv0.1.2\nv0.1.3\nv0.1.4\nv0.1.5\nv0.1.6\nv0.1.7", - "revision": "2fb7031e" - } - } - assert.deepEqual(HealthCheckUtils.getTcpPort(debug).error, true); - }); + it("returns an error when the port is misformated", async () => { + const debug = { + id: "a", + addrs: ["hello"], + repo: "", + spr: "", + announceAddresses: ["/ip4/127.0.0.1/tcp/8070"], + table: { + localNode: { + nodeId: "", + peerId: "", + record: "", + address: "0.0.0.0:8090", + seen: false, + }, + nodes: [], + }, + codex: { + version: + "v0.1.0\nv0.1.1\nv0.1.2\nv0.1.3\nv0.1.4\nv0.1.5\nv0.1.6\nv0.1.7", + revision: "2fb7031e", + }, + }; + assert.deepEqual(HealthCheckUtils.getTcpPort(debug).error, true); + }); - it("extracts the announced ip", async () => { - assert.deepEqual(HealthCheckUtils.extractAnnounceAddresses([ - "/ip4/127.0.0.1/tcp/8070" - ]).data, "127.0.0.1"); - assert.deepEqual(HealthCheckUtils.extractAnnounceAddresses([]).error, true); - assert.deepEqual(HealthCheckUtils.extractAnnounceAddresses(["hello"]).error, true); - }); -}) \ No newline at end of file + it("extracts the announced ip", async () => { + assert.deepEqual( + HealthCheckUtils.extractAnnounceAddresses(["/ip4/127.0.0.1/tcp/8070"]) + .data, + "127.0.0.1" + ); + assert.deepEqual(HealthCheckUtils.extractAnnounceAddresses([]).error, true); + assert.deepEqual( + HealthCheckUtils.extractAnnounceAddresses(["hello"]).error, + true + ); + }); +}); diff --git a/src/components/HealthChecks/health-check.utils.ts b/src/components/HealthChecks/health-check.utils.ts index 096eacb..7ded7ee 100644 --- a/src/components/HealthChecks/health-check.utils.ts +++ b/src/components/HealthChecks/health-check.utils.ts @@ -1,66 +1,83 @@ -import { CodexDebugInfo, SafeValue, CodexError } from "@codex-storage/sdk-js" +import { CodexDebugInfo, SafeValue, CodexError } from "@codex-storage/sdk-js"; export const HealthCheckUtils = { - removePort(url: string) { - const parts = url.split(":") - return parts[0] + ":" + parts[1] - }, + removePort(url: string) { + const parts = url.split(":"); + return parts[0] + ":" + parts[1]; + }, - /* - * Extract the port from a protocol + ip + port string - */ - getPort(url: string) { - return parseInt(url.split(":")[2] || "80", 10) - }, - - containsPort(url: string) { - return url.split(":").length > 2 - }, - - isUrlInvalid(url: string) { - try { - new URL(url) - return false - // We do not need to manage the error because we want to check - // if the URL is valid or not only. - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (_) { - return true - } - }, - - extractAnnounceAddresses(announceAddresses: string[]): SafeValue { - if (announceAddresses.length === 0) { - return { error: true, data: new CodexError("Not existing announce address") } - } - - - const ip = announceAddresses[0].split("/") - - if (ip.length !== 5) { - return { error: true, data: new CodexError("Misformatted ip") } - } - - return { error: false, data: ip[2] } - }, - - getTcpPort(info: CodexDebugInfo): SafeValue { - if (info.addrs.length === 0) { - return { error: true, data: new CodexError("Not existing address") } - } - - const parts = info.addrs[0].split("/") - - if (parts.length < 2) { - return { error: true, data: new CodexError("Address misformated") } - } - - const port = parseInt(parts[parts.length - 1], 10) - - if (isNaN(port)) { - return { error: true, data: new CodexError("Port misformated") } - } - - return { error: false, data: port } + extractBasicAuth(url: string) { + if (!url.includes("@")) { + return { auth: "", url }; } -} \ No newline at end of file + + const [prefix, rest] = url.split("@"); + const [protocol, auth] = prefix.split("//"); + + return { auth, url: protocol + "//" + rest }; + }, + + /* + * Extract the port from a protocol + ip + port string + */ + getPort(url: string) { + return url.split(":")[2] || ""; + }, + + containsPort(url: string) { + if (url.includes("@")) { + return url.split(":").length > 3; + } + + return url.split(":").length > 2; + }, + + isUrlInvalid(url: string) { + try { + new URL(url); + return false; + // We do not need to manage the error because we want to check + // if the URL is valid or not only. + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (_) { + return true; + } + }, + + extractAnnounceAddresses(announceAddresses: string[]): SafeValue { + if (announceAddresses.length === 0) { + return { + error: true, + data: new CodexError("Not existing announce address"), + }; + } + + const ip = announceAddresses[0].split("/"); + + if (ip.length !== 5) { + return { error: true, data: new CodexError("Misformatted ip") }; + } + + return { error: false, data: ip[2] }; + }, + + getTcpPort(info: CodexDebugInfo): SafeValue { + if (info.addrs.length === 0) { + return { error: true, data: new CodexError("Not existing address") }; + } + + const parts = info.addrs[0].split("/"); + + if (parts.length < 2) { + return { error: true, data: new CodexError("Address misformated") }; + } + + const port = parseInt(parts[parts.length - 1], 10); + + if (isNaN(port)) { + return { error: true, data: new CodexError("Port misformated") }; + } + + return { error: false, data: port }; + }, +}; diff --git a/src/components/ManifestFetch/ManifestFetch.tsx b/src/components/ManifestFetch/ManifestFetch.tsx index c611cad..acbe97f 100644 --- a/src/components/ManifestFetch/ManifestFetch.tsx +++ b/src/components/ManifestFetch/ManifestFetch.tsx @@ -17,7 +17,6 @@ export function ManifestFetch() { if (s.error === false) { setCid(""); queryClient.invalidateQueries({ queryKey: ["cids"] }); - console.info("Done"); } return Promises.rejectOnError(s); }); diff --git a/src/hooks/useDebug.ts b/src/hooks/useDebug.ts index 909335f..ffe0369 100644 --- a/src/hooks/useDebug.ts +++ b/src/hooks/useDebug.ts @@ -3,29 +3,31 @@ import { CodexSdk } from "../sdk/codex"; import { Promises } from "../utils/promises"; export function useDebug(throwOnError: boolean) { - const { data, isError, isPending, refetch, isSuccess, isFetching } = useQuery({ - queryFn: () => - CodexSdk.debug() - .info() - .then((s) => Promises.rejectOnError(s)), + const { data, isError, isPending, refetch, isSuccess, isFetching } = useQuery( + { + queryFn: () => + CodexSdk.debug() + .info() + .then((s) => Promises.rejectOnError(s)), - queryKey: ["debug"], + queryKey: ["debug"], - // No need to retry because if the connection to the node - // is back again, all the queries will be invalidated. - retry: false, + // No need to retry because if the connection to the node + // is back again, all the queries will be invalidated. + retry: false, - // The client node should be local, so display the cache value while - // making a background request looks good. - staleTime: 0, + // The client node should be local, so display the cache value while + // making a background request looks good. + staleTime: 0, - // Refreshing when focus returns can be useful if a user comes back - // to the UI after performing an operation in the terminal. - refetchOnWindowFocus: true, + // Refreshing when focus returns can be useful if a user comes back + // to the UI after performing an operation in the terminal. + refetchOnWindowFocus: true, - // Throw the error to the error boundary - throwOnError, - }); + // Throw the error to the error boundary + throwOnError, + } + ); - return { data, isPending, isError, isSuccess, refetch, isFetching }; + return { data, isPending, isError, isSuccess, refetch, isFetching }; } diff --git a/src/main.tsx b/src/main.tsx index da29709..fd9a768 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -135,7 +135,7 @@ if (rootElement) { return; } - return CodexSdk.updateURL(url); + return CodexSdk.updateURL(url, {}); } } } diff --git a/src/routes/dashboard/settings.tsx b/src/routes/dashboard/settings.tsx index 5478c67..90f4dbd 100644 --- a/src/routes/dashboard/settings.tsx +++ b/src/routes/dashboard/settings.tsx @@ -7,41 +7,46 @@ import Logotype from "../../assets/icons/logotype.svg?react"; import Logo from "../../assets/icons/logo.svg?react"; import { Versions } from "../../components/Versions/Versions"; import { BackgroundImage } from "../../components/BackgroundImage/BackgroundImage"; +import { useNetwork } from "../../network/useNetwork"; -export const SettingsRoute = () => ( -
-
-
- - -
- -
-
-

Personalization

- ( - - )}> - - +export const SettingsRoute = () => { + const online = useNetwork(); -

Connection

+ return ( +
+
+
+ + +
+ +
+
+

Personalization

+ ( + + )}> + + - ( - - )}> - {}} /> - -
+

Connection

- -
-); + ( + + )}> + {}} /> + +
+ + +
+ ); +}; diff --git a/src/sdk/codex.ts b/src/sdk/codex.ts index d4660f7..0a6fd59 100644 --- a/src/sdk/codex.ts +++ b/src/sdk/codex.ts @@ -4,38 +4,55 @@ import { WebStorage } from "../utils/web-storage"; let client: Codex = new Codex(import.meta.env.VITE_CODEX_API_URL); let url: string = import.meta.env.VITE_CODEX_API_URL; +type CodexSdkUpdateOptions = { + auth?: { + basic?: string; + }; +}; + export const CodexSdk = { url() { return url; }, - load() { - return WebStorage.get("codex-node-url").then((u) => { - url = u || import.meta.env.VITE_CODEX_API_URL; - client = new Codex(url); - }); + async load() { + const [url = import.meta.env.VITE_CODEX_API_URL, basicAuthSecret] = + await Promise.all([ + WebStorage.get("codex-node-url"), + WebStorage.get("codex-auth-basic"), + ]); + + client = new Codex(url, { auth: { basic: basicAuthSecret } }); }, - updateURL(u: string) { - url = u; - client = new Codex(url); + updateURL(u: string, options: CodexSdkUpdateOptions) { + let basicAuthSecret: string = ""; - return WebStorage.set("codex-node-url", url); + if (options.auth?.basic) { + basicAuthSecret = btoa(options.auth?.basic); + } + + url = u; + client = new Codex(url, { auth: { basic: basicAuthSecret } }); + + return WebStorage.set("codex-auth-basic", basicAuthSecret).then(() => + WebStorage.set("codex-node-url", url) + ); }, debug() { - return client.debug + return client.debug; }, data() { - return client.data + return client.data; }, node() { - return client.node + return client.node; }, marketplace() { - return client.marketplace + return client.marketplace; }, };