Add authentication with seed nodes

Signed-off-by: Sebastian Martinez <sebastinez@me.com>
This commit is contained in:
Sebastian Martinez
2022-05-18 15:45:47 +02:00
committed by Alexis Sellier
parent e0ee26f2a1
commit 5ef2b7512c
7 changed files with 111 additions and 16 deletions
+3
View File
@@ -8,3 +8,6 @@ coverage
# Integration Tests
cypress/screenshots
# Mac OS
.DS_Store
+13 -1
View File
@@ -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}
+7 -6
View File
@@ -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}
+36 -1
View File
@@ -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}
+46 -6
View File
@@ -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 }));
}
-2
View File
@@ -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 };
+6
View File
@@ -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));