Refactor api calls

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 3f28844dc8
commit e0ee26f2a1
6 changed files with 215 additions and 59 deletions
+1 -1
View File
@@ -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();
+46
View File
@@ -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>
+84 -36
View File
@@ -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://";
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);
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<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' }
});
}
// 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 {
+5 -5
View File
@@ -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> {
+17 -17
View File
@@ -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 {
+62
View File
@@ -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 };
}