mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-11-15 19:30:38 +08:00
Implement simplified authentication flow
We change the instructions to connect to a local node to use the simplified authentication flow. Signed-off-by: Thomas Scholtes <geigerzaehler@axiom.fm>
This commit is contained in:
@@ -6,7 +6,6 @@ import { HttpdClient } from "@httpd-client";
|
||||
import { authenticate } from "@httpd-client/tests/support/httpd.js";
|
||||
import { createPeerManager } from "@tests/support/peerManager.js";
|
||||
import { gitOptions } from "@tests/support/fixtures.js";
|
||||
import { sessionPayloadSchema } from "@httpd-client/lib/session";
|
||||
import { tmpDir } from "@tests/support/support.js";
|
||||
|
||||
describe("session", async () => {
|
||||
@@ -29,18 +28,7 @@ describe("session", async () => {
|
||||
});
|
||||
|
||||
test("#update(id, {sig, pk})", async () => {
|
||||
const { stdout } = await peer.rad(["web", "--backend", api.url, "--json"]);
|
||||
const session = sessionPayloadSchema.safeParse(JSON.parse(stdout));
|
||||
|
||||
if (!session.success) {
|
||||
throw new Error("Failed to parse session payload");
|
||||
}
|
||||
|
||||
const { sessionId, signature, publicKey } = session.data;
|
||||
await api.session.update(sessionId, {
|
||||
sig: signature,
|
||||
pk: publicKey,
|
||||
});
|
||||
await authenticate(api, peer);
|
||||
});
|
||||
|
||||
test("#delete(id)", async () => {
|
||||
|
||||
@@ -1,24 +1,31 @@
|
||||
import type { HttpdClient } from "@httpd-client";
|
||||
import type { RadiclePeer } from "@tests/support/peerManager.js";
|
||||
|
||||
import { sessionPayloadSchema } from "@httpd-client/lib/session.js";
|
||||
import assert from "node:assert";
|
||||
|
||||
export async function authenticate(
|
||||
api: HttpdClient,
|
||||
peer: RadiclePeer,
|
||||
): Promise<string> {
|
||||
const { stdout } = await peer.rad(["web", "--backend", api.url, "--json"]);
|
||||
const session = sessionPayloadSchema.safeParse(JSON.parse(stdout));
|
||||
const { stdout } = await peer.spawn("rad-web", [
|
||||
"http://localhost:3001",
|
||||
"--no-open",
|
||||
"--connect",
|
||||
"--listen",
|
||||
`${peer.httpdBaseUrl.hostname}:${peer.httpdBaseUrl.port}`,
|
||||
]);
|
||||
const match = stdout.match(/Visit (http:\/\/\S+) to connect/);
|
||||
assert(
|
||||
match !== null && match[1],
|
||||
`Failed to get authentication URL from: ${stdout}`,
|
||||
);
|
||||
|
||||
if (!session.success) {
|
||||
throw new Error("Failed to parse session payload");
|
||||
}
|
||||
const authUrl = new URL(match[1]);
|
||||
const sessionId = authUrl.pathname.split("/")[2];
|
||||
|
||||
const { sessionId, signature, publicKey } = session.data;
|
||||
await api.session.update(sessionId, {
|
||||
sig: signature,
|
||||
pk: publicKey,
|
||||
sig: authUrl.searchParams.get("sig")!,
|
||||
pk: authUrl.searchParams.get("pk")!,
|
||||
});
|
||||
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<script lang="ts">
|
||||
import * as httpd from "@app/lib/httpd";
|
||||
import * as modal from "@app/lib/modal";
|
||||
import { closeFocused } from "@app/components/Popover.svelte";
|
||||
import { httpdStore } from "@app/lib/httpd";
|
||||
|
||||
import Avatar from "@app/components/Avatar.svelte";
|
||||
import Button from "@app/components/Button.svelte";
|
||||
import ConnectModal from "@app/modals/ConnectModal.svelte";
|
||||
import Command from "@app/components/Command.svelte";
|
||||
import IconButton from "@app/components/IconButton.svelte";
|
||||
import IconSmall from "@app/components/IconSmall.svelte";
|
||||
import NodeId from "@app/components/NodeId.svelte";
|
||||
@@ -78,31 +77,23 @@
|
||||
</div>
|
||||
</div>
|
||||
</Popover>
|
||||
{:else if $httpdStore.state === "running"}
|
||||
<Button
|
||||
on:click={() => {
|
||||
modal.show({
|
||||
component: ConnectModal,
|
||||
props: {},
|
||||
});
|
||||
}}
|
||||
size="large"
|
||||
variant="secondary-toggle-off">
|
||||
<IconSmall name="key" />
|
||||
Authenticate
|
||||
<div class="indicator" />
|
||||
</Button>
|
||||
{:else}
|
||||
<Button
|
||||
on:click={() => {
|
||||
modal.show({
|
||||
component: ConnectModal,
|
||||
props: {},
|
||||
});
|
||||
}}
|
||||
size="large"
|
||||
variant="secondary-toggle-off">
|
||||
<IconSmall name="chat" />
|
||||
Authenticate
|
||||
</Button>
|
||||
<Popover popoverPositionTop="3rem" popoverPositionRight="0">
|
||||
<Button
|
||||
slot="toggle"
|
||||
let:toggle
|
||||
on:click={toggle}
|
||||
size="large"
|
||||
variant="secondary-toggle-off">
|
||||
<IconSmall name="key" />
|
||||
Authenticate
|
||||
<div class="indicator" />
|
||||
</Button>
|
||||
<div slot="popover" class="connect-popover">
|
||||
<div style:margin-bottom="1em">
|
||||
Authenticate with your local backend to make changes
|
||||
</div>
|
||||
<Command fullWidth command={`rad-web ${window.origin} --connect`} />
|
||||
</div>
|
||||
</Popover>
|
||||
{/if}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
import Button from "@app/components/Button.svelte";
|
||||
import Command from "@app/components/Command.svelte";
|
||||
import ExternalLink from "@app/components/ExternalLink.svelte";
|
||||
import IconButton from "@app/components/IconButton.svelte";
|
||||
import IconSmall from "@app/components/IconSmall.svelte";
|
||||
import Link from "@app/components/Link.svelte";
|
||||
@@ -33,17 +32,14 @@
|
||||
align-items: center;
|
||||
font-size: var(--font-size-small);
|
||||
}
|
||||
.label {
|
||||
display: block;
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: var(--font-weight-regular);
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
.status {
|
||||
font-size: var(--font-size-tiny);
|
||||
color: var(--color-fill-gray);
|
||||
text-align: left;
|
||||
}
|
||||
.connect-popover {
|
||||
font-size: var(--font-size-small);
|
||||
}
|
||||
</style>
|
||||
|
||||
{#if $httpdStore.state === "stopped"}
|
||||
@@ -58,17 +54,12 @@
|
||||
<IconSmall name="device" />
|
||||
Connect
|
||||
</Button>
|
||||
|
||||
<div slot="popover" style:width="23rem">
|
||||
<div class="label">
|
||||
Use the
|
||||
<ExternalLink href="https://radicle.xyz/#try">Radicle CLI</ExternalLink>
|
||||
to connect your device.
|
||||
<div slot="popover" class="connect-popover">
|
||||
<div style:margin-bottom="1em">
|
||||
Start the backend to browse projecs on your local machine, create
|
||||
issues, and participate in discussions.
|
||||
</div>
|
||||
<div class="label">
|
||||
Run the following command to start the httpd daemon.
|
||||
</div>
|
||||
<Command command="radicle-httpd" fullWidth />
|
||||
<Command fullWidth command={`rad-web ${window.origin}`} />
|
||||
</div>
|
||||
</Popover>
|
||||
{:else}
|
||||
|
||||
@@ -220,6 +220,7 @@ function urlToRoute(url: URL): Route | null {
|
||||
id,
|
||||
signature: url.searchParams.get("sig") ?? "",
|
||||
publicKey: url.searchParams.get("pk") ?? "",
|
||||
apiAddr: url.searchParams.get("addr") ?? "127.0.0.1:8080",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ export interface NotFoundRoute {
|
||||
|
||||
interface SessionRoute {
|
||||
resource: "session";
|
||||
params: { id: string; signature: string; publicKey: string };
|
||||
params: { id: string; signature: string; publicKey: string; apiAddr: string };
|
||||
}
|
||||
|
||||
export interface LoadErrorRoute {
|
||||
|
||||
@@ -1,193 +0,0 @@
|
||||
<script lang="ts">
|
||||
import * as httpd from "@app/lib/httpd";
|
||||
import * as modal from "@app/lib/modal";
|
||||
import { httpdStore } from "@app/lib/httpd";
|
||||
|
||||
import Command from "@app/components/Command.svelte";
|
||||
import Modal from "@app/components/Modal.svelte";
|
||||
import Icon from "@app/components/Icon.svelte";
|
||||
import IconButton from "@app/components/IconButton.svelte";
|
||||
import IconSmall from "@app/components/IconSmall.svelte";
|
||||
import Link from "@app/components/Link.svelte";
|
||||
import TextInput from "@app/components/TextInput.svelte";
|
||||
|
||||
$: customUrl = `${httpd.api.baseUrl.scheme}://${httpd.api.baseUrl.hostname}:${customPort}`;
|
||||
$: command = `rad web --frontend ${
|
||||
new URL(import.meta.url).origin
|
||||
} --backend ${customUrl}`;
|
||||
let customPort = httpd.api.port;
|
||||
$: validPortNumber = Number(customPort) > 0 && Number(customPort) <= 65535;
|
||||
|
||||
$: if ($httpdStore.state === "authenticated") {
|
||||
modal.hide();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 33rem;
|
||||
margin-top: 1.5rem;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
.progress {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.progress-bar {
|
||||
height: 6px;
|
||||
border-radius: var(--border-radius-round);
|
||||
background-color: var(--color-background-dip);
|
||||
}
|
||||
.bar {
|
||||
display: flex;
|
||||
background-color: var(--color-fill-secondary);
|
||||
height: 100%;
|
||||
border-radius: var(--border-radius-round);
|
||||
}
|
||||
.captions {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
color: var(--color-foreground-dim);
|
||||
font-size: var(--font-size-tiny);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.input {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.status {
|
||||
font-size: var(--font-size-tiny);
|
||||
color: var(--color-fill-gray);
|
||||
}
|
||||
.separator {
|
||||
height: 1px;
|
||||
background-color: var(--color-border-hint);
|
||||
}
|
||||
|
||||
.host {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: var(--font-size-small);
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: var(--font-size-small);
|
||||
}
|
||||
</style>
|
||||
|
||||
<Modal title="Connect and authenticate">
|
||||
<Icon name="key" size="48" slot="icon" />
|
||||
|
||||
<svelte:fragment slot="subtitle">
|
||||
Complete the steps below to browse projects on your local machine,
|
||||
<br />
|
||||
create issues, and participate in discussions.
|
||||
</svelte:fragment>
|
||||
|
||||
<div class="container" slot="body">
|
||||
{#if $httpdStore.state === "stopped"}
|
||||
<div class="progress">
|
||||
<div class="progress-bar">
|
||||
<div class="bar" style:width="1%" />
|
||||
</div>
|
||||
<div class="captions">
|
||||
<div
|
||||
style:text-align="left"
|
||||
style:color="var(--color-fill-secondary)">
|
||||
Start httpd server
|
||||
</div>
|
||||
<div>Authenticate</div>
|
||||
<div style:text-align="right">Done</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input">
|
||||
<div class="label">
|
||||
Run this command in your terminal to connect to your local node:
|
||||
</div>
|
||||
<Command fullWidth command="radicle-httpd" />
|
||||
</div>
|
||||
|
||||
<div class="input">
|
||||
<div class="label">Port:</div>
|
||||
<div style="width: 100%;">
|
||||
<TextInput
|
||||
name="port"
|
||||
size="small"
|
||||
bind:value={customPort}
|
||||
valid={validPortNumber}
|
||||
validationMessage="Invalid port"
|
||||
on:submit={() => httpd.changeHttpdPort(Number(customPort))}>
|
||||
<div
|
||||
slot="right"
|
||||
style="height: 100%; display: flex; align-items: center; padding: 0 0.5rem 0 0.25rem;">
|
||||
<IconSmall name="edit" />
|
||||
</div>
|
||||
</TextInput>
|
||||
</div>
|
||||
</div>
|
||||
{:else if $httpdStore.state === "running"}
|
||||
<div class="progress">
|
||||
<div class="progress-bar">
|
||||
<div class="bar" style:width="50%" />
|
||||
</div>
|
||||
<div class="captions">
|
||||
<div style:text-align="left">Start httpd server</div>
|
||||
<div style:color="var(--color-fill-secondary)">Authenticate</div>
|
||||
<div style:text-align="right">Done</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; flex-direction: column; gap: 0.5rem;">
|
||||
<div class="status">Httpd server running</div>
|
||||
<div class="host">
|
||||
radicle.local
|
||||
<Link
|
||||
on:afterNavigate={modal.hide}
|
||||
route={{
|
||||
resource: "nodes",
|
||||
params: {
|
||||
baseUrl: httpd.api.baseUrl,
|
||||
projectPageIndex: 0,
|
||||
},
|
||||
}}>
|
||||
<IconButton>Browse</IconButton>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div class="separator" />
|
||||
<div class="input">
|
||||
<div class="label">
|
||||
Run this command in your terminal to authenticate yourself:
|
||||
</div>
|
||||
<Command fullWidth {command} />
|
||||
</div>
|
||||
<div class="input">
|
||||
<div class="label">Port:</div>
|
||||
<div style="width: 100%;">
|
||||
<TextInput
|
||||
name="port"
|
||||
size="small"
|
||||
bind:value={customPort}
|
||||
valid={validPortNumber}
|
||||
validationMessage="Invalid port"
|
||||
on:submit={() => httpd.changeHttpdPort(Number(customPort))}>
|
||||
<div
|
||||
slot="right"
|
||||
style="height: 100%; display: flex; align-items: center; padding: 0 0.5rem 0 0.25rem;">
|
||||
<IconSmall name="edit" />
|
||||
</div>
|
||||
</TextInput>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Modal>
|
||||
@@ -14,6 +14,10 @@
|
||||
export let activeRoute: Extract<Route, { resource: "session" }>;
|
||||
|
||||
onMount(async () => {
|
||||
const port = Number.parseInt(activeRoute.params.apiAddr.split(":")[1]);
|
||||
if (port > 0 && port < 2 ** 16) {
|
||||
httpd.changeHttpdPort(port);
|
||||
}
|
||||
const isAuthenticated = await httpd.authenticate(activeRoute.params);
|
||||
|
||||
if (isAuthenticated) {
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { expect, test } from "@tests/support/fixtures.js";
|
||||
|
||||
test("rad web command reacts to port change", async ({ page, peerManager }) => {
|
||||
const peer = await peerManager.createPeer({
|
||||
name: "port-test",
|
||||
});
|
||||
await peer.startHttpd(8090);
|
||||
|
||||
await page.goto("/");
|
||||
await page.getByRole("button", { name: "Authenticate" }).click();
|
||||
|
||||
await expect(
|
||||
page.getByText(
|
||||
"rad web --frontend http://localhost:3001 --backend http://127.0.0.1:8081",
|
||||
),
|
||||
).toBeVisible();
|
||||
await page.locator('input[name="port"]').fill("8090");
|
||||
await page.locator('input[name="port"]').press("Enter");
|
||||
|
||||
await expect(
|
||||
page.getByText(
|
||||
"rad web --frontend http://localhost:3001 --backend http://127.0.0.1:8090",
|
||||
),
|
||||
).toBeVisible();
|
||||
|
||||
await peer.stopHttpd();
|
||||
});
|
||||
+14
-32
@@ -2,8 +2,8 @@
|
||||
import type * as Stream from "node:stream";
|
||||
import * as Fs from "node:fs/promises";
|
||||
import * as Path from "node:path";
|
||||
import assert from "node:assert";
|
||||
import { test as base, expect, type ViewportSize } from "@playwright/test";
|
||||
import { object, string, ZodSchema } from "zod";
|
||||
|
||||
import * as Process from "./process.js";
|
||||
import * as issue from "@tests/support/cobs/issue.js";
|
||||
@@ -20,18 +20,6 @@ const fixturesDir = Path.resolve(supportDir, "..", "./fixtures");
|
||||
|
||||
type ViewportTypes = "iPhoneXR" | "Desktop";
|
||||
|
||||
interface Auth {
|
||||
sessionId: string;
|
||||
publicKey: string;
|
||||
signature: string;
|
||||
}
|
||||
|
||||
const authSchema = object({
|
||||
sessionId: string(),
|
||||
publicKey: string(),
|
||||
signature: string(),
|
||||
}) satisfies ZodSchema<Auth>;
|
||||
|
||||
export const viewportSizes: Record<ViewportTypes, ViewportSize> = {
|
||||
iPhoneXR: { width: 414, height: 896 },
|
||||
Desktop: { width: 1280, height: 720 },
|
||||
@@ -167,28 +155,22 @@ export const test = base.extend<{
|
||||
|
||||
await peer.startHttpd();
|
||||
await peer.startNode();
|
||||
await page.goto("/");
|
||||
await page.getByRole("button", { name: "Authenticate" }).click();
|
||||
await page
|
||||
.locator('input[name="port"]')
|
||||
.fill(peer.httpdBaseUrl.port.toString());
|
||||
await page.locator('input[name="port"]').press("Enter");
|
||||
const { stdout } = await peer.spawn("rad-web", [
|
||||
"--frontend",
|
||||
"http://localhost:3001",
|
||||
"--backend",
|
||||
`${peer.httpdBaseUrl.scheme}://${peer.httpdBaseUrl.hostname}:${peer.httpdBaseUrl.port}`,
|
||||
"--json",
|
||||
"--no-open",
|
||||
"--connect",
|
||||
"--listen",
|
||||
`${peer.httpdBaseUrl.hostname}:${peer.httpdBaseUrl.port}`,
|
||||
]);
|
||||
const result = authSchema.safeParse(JSON.parse(stdout));
|
||||
if (result.success) {
|
||||
const { sessionId, publicKey, signature } = result.data;
|
||||
await page.goto(`/session/${sessionId}?pk=${publicKey}&sig=${signature}`);
|
||||
await expect(page.getByText("Authenticated")).toBeVisible();
|
||||
await page.getByRole("button", { name: "Close" }).click();
|
||||
} else {
|
||||
throw new Error("Not able to parse rad web output");
|
||||
}
|
||||
const match = stdout.match(
|
||||
/Open the following URL to connect: (http:\/\/\S+)/,
|
||||
);
|
||||
assert(
|
||||
match !== null && match[1],
|
||||
`Failed to get authentication URL from: ${stdout}`,
|
||||
);
|
||||
await page.goto(match[1]);
|
||||
await page.getByText("Successfully authenticated").waitFor();
|
||||
|
||||
await use(peer);
|
||||
},
|
||||
|
||||
@@ -1 +1 @@
|
||||
6ca7da276839e5217b340dbfff8cd9095d8464c5
|
||||
59f506dbb5591d3fe68e638038495730c455d72a
|
||||
|
||||
Reference in New Issue
Block a user