mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-11-16 03:40:37 +08:00
Add authentication with seed nodes
Signed-off-by: Sebastian Martinez <sebastinez@me.com>
This commit is contained in:
@@ -8,3 +8,6 @@ coverage
|
||||
|
||||
# Integration Tests
|
||||
cypress/screenshots
|
||||
|
||||
# Mac OS
|
||||
.DS_Store
|
||||
|
||||
+13
-1
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
@@ -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 }));
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user