Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Add authentication with seed nodes
Sebastian Martinez committed 4 years ago
commit 5ef2b7512cd1d9d9f754bc21904a77690424d1e7
parent e0ee26f2a17408e87d70232dc7b1b9300561f049
7 files changed +111 -16
modified .gitignore
@@ -8,3 +8,6 @@ coverage

# Integration Tests
cypress/screenshots
+

+
# Mac OS
+
.DS_Store
modified src/Header.svelte
@@ -13,12 +13,18 @@
  import Search from '@app/Search.svelte';
  import Icon from "./Icon.svelte";
  import MobileNavbar from "./MobileNavbar.svelte";
+
  import SeedDropdown from "./SeedDropdown.svelte";

  export let session: Session | null;
  export let config: Config;

  let sessionButtonHover = false;
  let mobileNavbarDisplayed = false;
+
  let seedDropdown = false;
+

+
  function toggleDropdown() {
+
    seedDropdown = !seedDropdown;
+
  }

  function toggleNavbar() {
    mobileNavbarDisplayed = !mobileNavbarDisplayed;
@@ -118,7 +124,7 @@
    display: none;
  }
  @media(max-width: 800px) {
-
    .balance {
+
    .balance + .seeds {
      display: none;
    }
  }
@@ -181,6 +187,12 @@
        {/if}
      </span>

+
      {#if session && Object.keys(session.siwe).length > 0}
+
        <div class="seeds">
+
          <SeedDropdown seeds={session.siwe} {toggleDropdown} {seedDropdown} {config} />
+
        </div>
+
      {/if}
+

      <button class="address outline small"
        on:click={() => disconnectWallet(config)}
        on:mouseover={() => sessionButtonHover = true}
modified src/SeedDropdown.svelte
@@ -1,8 +1,8 @@
<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 Dropdown from "@app/Dropdown.svelte";
  import type { SeedSession } from "@app/siwe";

  export let seeds: { [key: string]: SeedSession };
@@ -10,8 +10,8 @@
  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 () => {
+
  // When a user signs into a new seed we want to update the seed listing
+
  $: 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}`;
@@ -29,16 +29,17 @@

<div class="selector">
  <span>
-
    <button class="small" on:click={toggleDropdown}>
+
    <button class="seed outline small" on:click={toggleDropdown}>
      Seeds
    </button>
    {#await formatSeeds() then items}
      <Dropdown
        {items}
+
        selected={null}
        visible={seedDropdown}
-
        on:select={({ detail }) => {
+
        on:select={(item) => {
          seedDropdown = false;
-
          navigate(`/seeds/${detail}`);
+
          navigate(`/seeds/${item.detail}`);
        }}
      />
    {/await}
modified src/base/seeds/View.svelte
@@ -7,10 +7,26 @@
  import NotFound from "@app/NotFound.svelte";
  import Clipboard from "@app/Clipboard.svelte";
  import Projects from "@app/base/orgs/View/Projects.svelte";
+
  import { SeedSession, signInWithEthereum } from "@app/siwe";
+
  import { session } from "@app/session";
+
  import Address from "@app/Address.svelte";

  export let config: Config;
  export let host: string;

+
  let sessionData: SeedSession | null = null;
+

+
  $: if ($session) {
+
    const entries = Object.entries($session.siwe);
+
    const result = entries.find(([, session]) => session.domain === host);
+
    if (result) {
+
      sessionData = result[1];
+
    }
+
  }
+

+
  const signIn = async (seed: Seed) => {
+
    await signInWithEthereum(seed, config);
+
  };
</script>

<style>
@@ -106,10 +122,29 @@
      <div class="label">Version</div>
      <div>{seed.version}</div>
      <div class="desktop" />
+
      <!-- User Session -->
+
      <div class="label">Connection</div>
+
      {#if sessionData}
+
        <div class="desktop"><Address address={sessionData.address} resolve {config} /></div>
+
        <div class="mobile"><Address address={sessionData.address} compact resolve {config} /></div>
+
        <div class="desktop" />
+
      {:else}
+
        <div class="subtle">Not connected</div>
+
        {#if config.signer}
+
          <div class="desktop">
+
            <button class="tiny secondary" on:click={() => signIn(seed)}>
+
              Sign in with Ethereum
+
            </button>
+
          </div>
+
        {/if}
+
      {/if}
    </div>
    <!-- Seed Projects -->
    <Projects {seed} {config} />
  </main>
{:catch}
-
  <NotFound title={host} subtitle="Not able to query information from this seed." />
+
  <NotFound
+
    title={host}
+
    subtitle="Not able to query information from this seed."
+
  />
{/await}
modified src/session.ts
@@ -7,6 +7,7 @@ import { Unreachable, assert, assertEq } from "@app/error";
import type { TypedDataSigner } from '@ethersproject/abstract-signer';
import type { WalletConnectSigner } from "./WalletConnectSigner";
import * as ethers from "ethers";
+
import type { SeedSession } from "./siwe";

export enum Connection {
  Disconnected,
@@ -31,6 +32,7 @@ export type State =
export interface Session {
  address: string;
  signer: Signer | null;
+
  siwe: { [key: string]: SeedSession };
  tokenBalance: BigNumber | null; // `null` means it isn't loaded yet.
  tx: TxState;
}
@@ -39,6 +41,7 @@ export interface Store extends Readable<State> {
  connectMetamask(config: Config): Promise<void>;
  connectWalletConnect(config: Config): Promise<void>;
  updateBalance(n: BigNumber): void;
+
  connectSeed(seed: { id: string; session: SeedSession }): void;
  refreshBalance(config: Config): Promise<void>;
  setTxSigning(): void;
  setTxPending(tx: TransactionResponse): void;
@@ -48,6 +51,7 @@ export interface Store extends Readable<State> {

export const loadState = (initial: State): Store => {
  const store = writable<State>(initial);
+
  const siwe = loadSeedSessions();

  return {
    subscribe: store.subscribe,
@@ -58,7 +62,7 @@ export const loadState = (initial: State): Store => {
      if (config.metamask.connected) {
        const metamask = config.metamask.session;
        const tokenBalance: BigNumber = await config.token.balanceOf(metamask.address);
-
        const session = { tokenBalance, tx: null, seeds: {}, signer: config.metamask.signer, address: metamask.address };
+
        const session = { tokenBalance, tx: null, siwe, signer: config.metamask.signer, address: metamask.address };

        store.set({ connection: Connection.Connected, session });
        config.setSigner(config.metamask.signer);
@@ -82,7 +86,7 @@ export const loadState = (initial: State): Store => {
        config.walletConnect.state.set({ state: "close" });

        const tokenBalance: BigNumber = await config.token.balanceOf(address);
-
        const session = { address, signer: config.metamask.signer, seeds: {}, tokenBalance, tx: null };
+
        const session = { address, signer: config.metamask.signer, siwe, tokenBalance, tx: null };

        store.set({
          connection: Connection.Connected,
@@ -104,7 +108,7 @@ export const loadState = (initial: State): Store => {

        const address = await signer.getAddress();
        const tokenBalance: BigNumber = await config.token.balanceOf(address);
-
        const session = { address, signer, seeds: {}, tokenBalance, tx: null };
+
        const session = { address, signer, siwe, tokenBalance, tx: null };
        const network = ethers.providers.getNetwork(
          signer.walletConnect.chainId
        );
@@ -153,6 +157,20 @@ export const loadState = (initial: State): Store => {
      }
    },

+
    connectSeed: ({ id, session }: { id: string; session: SeedSession }) => {
+
      store.update((s: State) => {
+
        switch (s.connection) {
+
          case Connection.Connected:
+
            s.session.siwe[id] = session;
+
            saveSession(s.session);
+

+
            return s;
+
          default:
+
            return s;
+
        }
+
      });
+
    },
+

    updateBalance: (n: BigNumber) => {
      store.update((s: State) => {
        assert(s.connection === Connection.Connected);
@@ -160,6 +178,7 @@ export const loadState = (initial: State): Store => {
          // If the token balance is loaded, we can update it, otherwise
          // we let it finish loading.
          s.session.tokenBalance = s.session.tokenBalance.add(n);
+
          saveSession(s.session);
        }
        return s;
      });
@@ -285,6 +304,26 @@ export async function changeAccounts(address: string): Promise<void> {
  state.refreshBalance(config);
}

+
export function loadSeedSessions(): { [key: string]: SeedSession } {
+
  const siweStorage = localStorage.getItem("siwe");
+

+
  if (siweStorage) {
+
    const siwe: { [key: string]: SeedSession } = JSON.parse(siweStorage);
+

+
    // We only keep the sessions that are still valid and remove expired from localStorage
+
    const activeSessions = Object.fromEntries(Object.entries(siwe).filter(([, value]) => value.expiration_time > Date.now() / 1000));
+
    window.localStorage.setItem("siwe", JSON.stringify({ ...activeSessions }));
+

+
    return activeSessions;
+
  }
+

+
  return {};
+
}
+

+
export async function connectSeed(seedSession: { id: string; session: SeedSession }): Promise<void> {
+
  state.connectSeed(seedSession);
+
}
+

state.subscribe(s => {
  console.log("session.state", s);
});
@@ -316,7 +355,8 @@ export function disconnectWallet(config: Config): void {
}

function saveSession(session: Session): void {
-
  window.localStorage.setItem("metamask", JSON.stringify({
-
    ...session, tokenBalance: null, config: null
-
  }));
+
  const { address, tokenBalance, tx, siwe } = session;
+

+
  window.localStorage.setItem("metamask", JSON.stringify({ address, tokenBalance, tx }));
+
  window.localStorage.setItem("siwe", JSON.stringify({ ...siwe }));
}
modified src/siwe.ts
@@ -54,8 +54,6 @@ export async function signInWithEthereum(seed: Seed, config: Config): Promise<{
  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 };
modified src/utils.ts
@@ -98,6 +98,12 @@ export function formatSeedId(id: string): string {
    + id.substring(id.length - 6, id.length);
}

+
export function removePrefix(hash: string): string {
+
  if (! hash.startsWith("0x")) { return hash; }
+

+
  return hash.substring(2);
+
}
+

export function formatRadicleUrn(id: string): string {
  assert(isRadicleId(id));