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:
Thomas Scholtes
2023-12-19 13:49:53 +00:00
committed by Rūdolfs Ošiņš
parent 448c762831
commit 2b03aec645
11 changed files with 66 additions and 322 deletions
+1 -13
View File
@@ -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 () => {
+17 -10
View File
@@ -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;
}
+19 -28
View File
@@ -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}
+8 -17
View File
@@ -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}
+1
View File
@@ -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",
},
};
}
+1 -1
View File
@@ -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 {
-193
View File
@@ -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>
+4
View File
@@ -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) {
-27
View File
@@ -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
View File
@@ -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
View File
@@ -1 +1 @@
6ca7da276839e5217b340dbfff8cd9095d8464c5
59f506dbb5591d3fe68e638038495730c455d72a