Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Refactor api calls
Sebastian Martinez committed 4 years ago
commit e0ee26f2a17408e87d70232dc7b1b9300561f049
parent 3f28844dc816722cead410760fa41c45f20fe6c2
6 files changed +216 -60
modified 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();
added src/SeedDropdown.svelte
@@ -0,0 +1,46 @@
+
<script lang="ts">
+
  import { navigate } from "svelte-routing";
+
  import { Seed } from "@app/base/seeds/Seed";
+
  import Dropdown from "@app/Dropdown.svelte";
+
  import type { Config } from "@app/config";
+
  import type { SeedSession } from "@app/siwe";
+

+
  export let seeds: { [key: string]: SeedSession };
+
  export let seedDropdown = false;
+
  export let toggleDropdown: () => void;
+
  export let config: Config;
+

+
  // When a user signs into a new seed, we want to update the seed listing
+
  const formatSeeds = async () => {
+
    return await Promise.all(Object.values(seeds).map(async session => {
+
      let seed = await Seed.lookup(session.domain, config);
+
      let key = `${seed.emoji} ${seed.host}`;
+

+
      return { key, value: seed.host, badge: null };
+
    }));
+
  };
+
</script>
+

+
<style>
+
  .selector {
+
    margin-left: 2rem;
+
  }
+
</style>
+

+
<div class="selector">
+
  <span>
+
    <button class="small" on:click={toggleDropdown}>
+
      Seeds
+
    </button>
+
    {#await formatSeeds() then items}
+
      <Dropdown
+
        {items}
+
        visible={seedDropdown}
+
        on:select={({ detail }) => {
+
          seedDropdown = false;
+
          navigate(`/seeds/${detail}`);
+
        }}
+
      />
+
    {/await}
+
  </span>
+
</div>
modified src/api.ts
@@ -3,50 +3,98 @@ export interface Host {
  port: number | null;
}

-
export async function get(
-
  path: string,
-
  params: Record<string, any>,
-
  api: Host
-
): Promise<any> {
-
  const query: Record<string, string> = {};
-
  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://";
-

-
  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, {
+
  async get(
+
    params: Record<string, any> = {},
+
    headers: Record<string, string> = {},
+
  ): Promise<any> {
+
    const query = this.formatParams(params);
+
    const search = new URLSearchParams(query).toString();
+
    const urlString = this.createUrl(search);
+

+
    return await Request.exec(urlString, {
      method: 'GET',
-
      headers: {
-
        'Accept': 'application/json',
-
      }
+
      headers: { ...headers, 'Accept': 'application/json' }
+
    });
+
  }
+

+
  async post(
+
    params: Record<string, any> = {},
+
    headers: Record<string, string> = {},
+
  ): Promise<any> {
+
    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<string, any> = {},
+
    headers: Record<string, string> = {},
+
  ): Promise<any> {
+
    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' }
    });
-
  } catch (err) {
-
    throw new ApiError("API request failed", urlString);
  }

-
  if (! response.ok) {
-
    throw new ApiError("Not found", urlString);
+
  // Executes a request and returns the response.
+
  static async exec(urlString: string, props: Record<string, any>): Promise<any> {
+
    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<string, any>): Record<string, string> {
+
    const query: Record<string, string> = {};
+
    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 {
modified 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<Seed> {
modified 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<ProjectInfo> {
-
    const info = await api.get(`projects/${nameOrUrn}`, {}, host);
+
  static async getInfo(nameOrUrn: string, host: Host): Promise<ProjectInfo> {
+
    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<ProjectInfo[]> {
-
    return api.get("projects", {}, host);
+
  static async getProjects(host: Host): Promise<ProjectInfo[]> {
+
    return new Request("projects", host).get();
  }

-
  static async getDelegateProjects(delegate: string, host: api.Host): Promise<ProjectInfo[]> {
-
    return api.get(`delegates/${delegate}/projects`, {}, host);
+
  static async getDelegateProjects(delegate: string, host: Host): Promise<ProjectInfo[]> {
+
    return new Request(`delegates/${delegate}/projects`, host).get();
  }

-
  static async getRemote(urn: string, peer: string, host: api.Host): Promise<Remote> {
-
    return api.get(`projects/${urn}/remotes/${peer}`, {}, host);
+
  static async getRemote(urn: string, peer: string, host: Host): Promise<Remote> {
+
    return new Request(`projects/${urn}/remotes/${peer}`, host).get();
  }

-
  static async getRemotes(urn: string, host: api.Host): Promise<Peer[]> {
-
    return api.get(`projects/${urn}/remotes`, {}, host);
+
  static async getRemotes(urn: string, host: Host): Promise<Peer[]> {
+
    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<Commit> {
-
    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<Tree> {
    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<Blob> {
-
    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<Blob> {
-
    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 {
added 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 };
+
}