diff --git a/src/Dropdown.svelte b/src/Dropdown.svelte
index f5fe65266..6a99b26db 100644
--- a/src/Dropdown.svelte
+++ b/src/Dropdown.svelte
@@ -2,7 +2,7 @@
import { createEventDispatcher } from "svelte";
export let items: { key: string; value: string; badge: string | null }[];
- export let selected: string | null;
+ export let selected: string | null = null;
export let visible = false;
const dispatch = createEventDispatcher();
diff --git a/src/SeedDropdown.svelte b/src/SeedDropdown.svelte
new file mode 100644
index 000000000..2f1284c9b
--- /dev/null
+++ b/src/SeedDropdown.svelte
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+ {#await formatSeeds() then items}
+ {
+ seedDropdown = false;
+ navigate(`/seeds/${detail}`);
+ }}
+ />
+ {/await}
+
+
diff --git a/src/api.ts b/src/api.ts
index 4fa1ef13b..4978f0c49 100644
--- a/src/api.ts
+++ b/src/api.ts
@@ -3,50 +3,98 @@ export interface Host {
port: number | null;
}
-export async function get(
- path: string,
- params: Record,
- api: Host
-): Promise {
- const query: Record = {};
- for (const [key, val] of Object.entries(params)) {
- if (val !== undefined && val !== null) {
- query[key] = val.toString();
- }
+export class Request {
+ path: string;
+ base: string;
+ protocol: string;
+ port: number;
+
+ constructor(path: string, api: Host) {
+ this.port = api.port || 8777;
+ this.base = api.host;
+ this.path = path.startsWith("/") ? path.slice(1) : path;
+ this.protocol = api.host === "0.0.0.0" ? "http://" : "https://";
}
- const base = api.host;
- const port = api.port;
- const search = new URLSearchParams(query).toString();
- // Allow using the functionality with local runned http-api
- const isLocalhost = /^0.0.0.0$/.test(base);
- const protocol = isLocalhost ? "http://" : "https://";
+ async get(
+ params: Record = {},
+ headers: Record = {},
+ ): Promise {
+ const query = this.formatParams(params);
+ const search = new URLSearchParams(query).toString();
+ const urlString = this.createUrl(search);
- path = path.startsWith("/") ? path.slice(1) : path;
-
- const baseUrl = path
- ? `${protocol}${base}/v1/${path}`
- : `${protocol}${base}`;
- const url = new URL(search ? `${baseUrl}?${search}` : baseUrl);
- url.port = String(port);
-
- const urlString = String(url);
- let response = null;
- try {
- response = await fetch(urlString, {
+ return await Request.exec(urlString, {
method: 'GET',
- headers: {
- 'Accept': 'application/json',
- }
+ headers: { ...headers, 'Accept': 'application/json' }
});
- } catch (err) {
- throw new ApiError("API request failed", urlString);
}
- if (! response.ok) {
- throw new ApiError("Not found", urlString);
+ async post(
+ params: Record = {},
+ headers: Record = {},
+ ): Promise {
+ const body = this.formatParams(params);
+ const urlString = this.createUrl();
+
+ return await Request.exec(urlString, {
+ method: 'POST',
+ body: JSON.stringify(body),
+ headers: { ...headers, 'Content-Type': 'application/json' }
+ });
+ }
+
+ async put(
+ params: Record = {},
+ headers: Record = {},
+ ): Promise {
+ const body = this.formatParams(params);
+ const urlString = this.createUrl();
+
+ return await Request.exec(urlString, {
+ method: 'PUT',
+ body: JSON.stringify(body),
+ headers: { ...headers, 'Content-Type': 'application/json' }
+ });
+ }
+
+ // Executes a request and returns the response.
+ static async exec(urlString: string, props: Record): Promise {
+ let response = null;
+ try {
+ response = await fetch(urlString, props);
+ } catch (err) {
+ throw new ApiError("API request failed", urlString);
+ }
+
+ if (! response.ok) {
+ throw new ApiError("Not found", urlString);
+ }
+ return response.json();
+ }
+
+ // Filters out undefined and null values.
+ private formatParams(params: Record): Record {
+ const query: Record = {};
+ for (const [key, val] of Object.entries(params)) {
+ if (val !== undefined && val !== null) {
+ query[key] = val.toString();
+ }
+ }
+
+ return query;
+ }
+
+ // Creates a URL with an eventual query string and port.
+ private createUrl(search?: string): string {
+ const baseUrl = this.path
+ ? `${this.protocol}${this.base}/v1/${this.path}`
+ : `${this.protocol}${this.base}`;
+
+ const url = new URL(search ? `${baseUrl}?${search}` : baseUrl);
+ url.port = String(this.port);
+ return String(url);
}
- return response.json();
}
export class ApiError extends Error {
diff --git a/src/base/seeds/Seed.ts b/src/base/seeds/Seed.ts
index 95438402c..74cc6bb52 100644
--- a/src/base/seeds/Seed.ts
+++ b/src/base/seeds/Seed.ts
@@ -1,4 +1,4 @@
-import * as api from '@app/api';
+import { Request, type Host } from '@app/api';
import type { Config } from '@app/config';
import * as proj from "@app/project";
import { isDomain } from '@app/utils';
@@ -108,12 +108,12 @@ export class Seed {
return result.map((project: proj.ProjectInfo) => ({ ...project, id: project.urn }));
}
- static async getPeer({ host, port }: api.Host): Promise<{ id: string }> {
- return api.get("/peer", {}, { host, port });
+ static async getPeer(host: Host): Promise<{ id: string }> {
+ return new Request("/peer", host).get();
}
- static async getInfo({ host, port }: api.Host): Promise<{ version: string }> {
- return api.get("/", {}, { host, port });
+ static async getInfo(host: Host): Promise<{ version: string }> {
+ return new Request("/", host).get();
}
static async lookup(hostname: string, cfg: Config): Promise {
diff --git a/src/project.ts b/src/project.ts
index 0e0494cf9..1e8ad8abb 100644
--- a/src/project.ts
+++ b/src/project.ts
@@ -1,6 +1,6 @@
import { navigate } from 'svelte-routing';
import { get, writable } from 'svelte/store';
-import * as api from '@app/api';
+import { type Host, Request } from '@app/api';
import type { Commit, CommitHeader, CommitsHistory } from '@app/commit';
import { isOid, isRadicleId } from '@app/utils';
import { Profile, ProfileType } from '@app/profile';
@@ -249,8 +249,8 @@ export class Project implements ProjectInfo {
return { tree, commit };
}
- static async getInfo(nameOrUrn: string, host: api.Host): Promise {
- const info = await api.get(`projects/${nameOrUrn}`, {}, host);
+ static async getInfo(nameOrUrn: string, host: Host): Promise {
+ const info = await new Request(`projects/${nameOrUrn}`, host).get();
return {
...info,
@@ -258,25 +258,25 @@ export class Project implements ProjectInfo {
};
}
- static async getProjects(host: api.Host): Promise {
- return api.get("projects", {}, host);
+ static async getProjects(host: Host): Promise {
+ return new Request("projects", host).get();
}
- static async getDelegateProjects(delegate: string, host: api.Host): Promise {
- return api.get(`delegates/${delegate}/projects`, {}, host);
+ static async getDelegateProjects(delegate: string, host: Host): Promise {
+ return new Request(`delegates/${delegate}/projects`, host).get();
}
- static async getRemote(urn: string, peer: string, host: api.Host): Promise {
- return api.get(`projects/${urn}/remotes/${peer}`, {}, host);
+ static async getRemote(urn: string, peer: string, host: Host): Promise {
+ return new Request(`projects/${urn}/remotes/${peer}`, host).get();
}
- static async getRemotes(urn: string, host: api.Host): Promise {
- return api.get(`projects/${urn}/remotes`, {}, host);
+ static async getRemotes(urn: string, host: Host): Promise {
+ return new Request(`projects/${urn}/remotes`, host).get();
}
static async getCommits(
urn: string,
- host: api.Host,
+ host: Host,
opts?: {
parent?: string | null;
since?: string;
@@ -294,11 +294,11 @@ export class Project implements ProjectInfo {
"page": opts?.page,
"verified": opts?.verified
};
- return api.get(`projects/${urn}/commits`, params, host);
+ return new Request(`projects/${urn}/commits`, host).get(params);
}
async getCommit(commit: string): Promise {
- return api.get(`projects/${this.urn}/commits/${commit}`, {}, this.seed.api);
+ return new Request(`projects/${this.urn}/commits/${commit}`, this.seed.api).get();
}
async getTree(
@@ -306,7 +306,7 @@ export class Project implements ProjectInfo {
path: string,
): Promise {
if (path === "/") path = "";
- return api.get(`projects/${this.urn}/tree/${commit}/${path}`, {}, this.seed.api);
+ return new Request(`projects/${this.urn}/tree/${commit}/${path}`, this.seed.api).get();
}
async getBlob(
@@ -314,13 +314,13 @@ export class Project implements ProjectInfo {
path: string,
options: { highlight: boolean },
): Promise {
- return api.get(`projects/${this.urn}/blob/${commit}/${path}`, options, this.seed.api);
+ return new Request(`projects/${this.urn}/blob/${commit}/${path}`, this.seed.api).get(options);
}
async getReadme(
commit: string,
): Promise {
- return api.get(`projects/${this.urn}/readme/${commit}`, {}, this.seed.api);
+ return new Request(`projects/${this.urn}/readme/${commit}`, this.seed.api).get();
}
navigateTo(browse: BrowseTo): void {
diff --git a/src/siwe.ts b/src/siwe.ts
new file mode 100644
index 000000000..e625d961b
--- /dev/null
+++ b/src/siwe.ts
@@ -0,0 +1,62 @@
+/* eslint-disable @typescript-eslint/naming-convention */
+import { SiweMessage } from "siwe";
+import { Request, type Host } from '@app/api';
+import type { Config } from "@app/config";
+import { removePrefix } from "@app/utils";
+import { connectSeed } from "./session";
+import type { Seed } from "./base/seeds/Seed";
+
+export interface SeedSession {
+ domain: string;
+ address: string;
+ statement: string;
+ uri: string;
+ version: string;
+ chain_id: string;
+ nonce: string;
+ issued_at: number;
+ expiration_time: number;
+ resources: string[];
+}
+
+export function createSiweMessage(seed: Seed, address: string, nonce: string, config: Config): string {
+ const date = new Date();
+ const expirationTime = new Date(date.getFullYear(), date.getMonth(), date.getDate() + 7).toISOString();
+
+ const message = new SiweMessage({
+ domain: seed.api.host,
+ address,
+ statement: `${seed.api.host} wants you to sign in with Ethereum`,
+ uri: window.location.origin,
+ nonce,
+ version: '1',
+ resources: [`rad:git:${seed.id}`],
+ expirationTime,
+ chainId: config.network.chainId
+ });
+
+ return message.prepareMessage();
+}
+
+export async function createUnauthorizedSession(host: Host): Promise<{ nonce: string; id: string }> {
+ return await new Request(`sessions`, host).post();
+}
+
+/// Signs the user into given seed and returns when succesfull a session id
+export async function signInWithEthereum(seed: Seed, config: Config): Promise<{ id: string } | null> {
+ if (! config.signer) {
+ return null;
+ }
+
+ const result = await createUnauthorizedSession(seed.api);
+ const address = await config.signer.getAddress();
+ const message = createSiweMessage(seed, address, result.nonce, config);
+ const signature = await config.signer.signMessage(message);
+
+ const auth: { id: string; session: SeedSession } = await new Request(`sessions/${result.id}`, seed.api).put({ message, signature: removePrefix(signature) });
+
+ await new Request(`sessions`, seed.api).get({}, { Authorization: result.id });
+ connectSeed({ id: result.id, session: auth.session });
+
+ return { id: result.id };
+}