remove ofetch, replace with fetch

This commit is contained in:
Jorrin
2024-04-19 20:41:21 +02:00
parent eea4eab60b
commit 75f5256b20
15 changed files with 132 additions and 111 deletions

View File

@@ -2,29 +2,18 @@ import { Stack, useLocalSearchParams } from "expo-router";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { H4, Paragraph, Text, View } from "tamagui"; import { H4, Paragraph, Text, View } from "tamagui";
import { getBackendMeta } from "@movie-web/api";
import ScreenLayout from "~/components/layout/ScreenLayout"; import ScreenLayout from "~/components/layout/ScreenLayout";
import { MWButton } from "~/components/ui/Button"; import { MWButton } from "~/components/ui/Button";
import { MWCard } from "~/components/ui/Card"; import { MWCard } from "~/components/ui/Card";
// TODO: extract to function with cleanup and types
const getBackendMeta = (
url: string,
): Promise<{
description: string;
hasCaptcha: boolean;
name: string;
url: string;
}> => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return fetch(`${url}/meta`).then((res) => res.json());
};
export default function Page() { export default function Page() {
const { url } = useLocalSearchParams(); const { url } = useLocalSearchParams<{ url: string }>();
const meta = useQuery({ const meta = useQuery({
queryKey: ["backendMeta", url], queryKey: ["backendMeta", url],
queryFn: () => getBackendMeta(url as string), queryFn: () => getBackendMeta(url),
}); });
return ( return (
@@ -102,7 +91,7 @@ export default function Page() {
gap="$4" gap="$4"
> >
<MWButton type="purple">I trust this server</MWButton> <MWButton type="purple">I trust this server</MWButton>
<MWButton type="secondary">Go back</MWButton> <MWButton type="cancel">Go back</MWButton>
<Paragraph color="$ash50" textAlign="center" fontWeight="$semibold"> <Paragraph color="$ash50" textAlign="center" fontWeight="$semibold">
Already have an account?{" "} Already have an account?{" "}

View File

@@ -4,35 +4,35 @@ export const MWButton = styled(Button, {
variants: { variants: {
type: { type: {
primary: { primary: {
backgroundColor: "$buttonPrimaryBackground", backgroundColor: "white",
color: "$buttonPrimaryText", color: "black",
fontWeight: "bold", fontWeight: "bold",
pressStyle: { pressStyle: {
backgroundColor: "$buttonPrimaryBackgroundHover", backgroundColor: "$silver100",
}, },
}, },
secondary: { secondary: {
backgroundColor: "$buttonSecondaryBackground", backgroundColor: "$ash700",
color: "$buttonSecondaryText", color: "$silver300",
fontWeight: "bold", fontWeight: "bold",
pressStyle: { pressStyle: {
backgroundColor: "$buttonSecondaryBackgroundHover", backgroundColor: "$ash500",
}, },
}, },
purple: { purple: {
backgroundColor: "$buttonPurpleBackground", backgroundColor: "$purple500",
color: "white", color: "white",
fontWeight: "bold", fontWeight: "bold",
pressStyle: { pressStyle: {
backgroundColor: "$buttonPurpleBackgroundHover", backgroundColor: "$purple400",
}, },
}, },
cancel: { cancel: {
backgroundColor: "$buttonCancelBackground", backgroundColor: "$ash500",
color: "white", color: "white",
fontWeight: "bold", fontWeight: "bold",
pressStyle: { pressStyle: {
backgroundColor: "$buttonCancelBackgroundHover", backgroundColor: "$ash300",
}, },
}, },
}, },

View File

@@ -32,7 +32,6 @@
"dependencies": { "dependencies": {
"@noble/hashes": "^1.4.0", "@noble/hashes": "^1.4.0",
"@scure/bip39": "^1.3.0", "@scure/bip39": "^1.3.0",
"node-forge": "^1.3.1", "node-forge": "^1.3.1"
"ofetch": "^1.3.4"
} }
} }

View File

@@ -1,6 +1,5 @@
import { ofetch } from "ofetch";
import type { LoginResponse } from "./types"; import type { LoginResponse } from "./types";
import { f } from "./fetch";
export function getAuthHeaders(token: string): Record<string, string> { export function getAuthHeaders(token: string): Record<string, string> {
return { return {
@@ -13,12 +12,12 @@ export async function accountLogin(
id: string, id: string,
deviceName: string, deviceName: string,
): Promise<LoginResponse> { ): Promise<LoginResponse> {
return ofetch<LoginResponse>("/auth/login", { return f<LoginResponse>("/auth/login", {
method: "POST", method: "POST",
body: { body: {
id, id,
device: deviceName, device: deviceName,
}, },
baseURL: url, baseUrl: url,
}); });
} }

View File

@@ -1,5 +1,3 @@
import { ofetch } from "ofetch";
import type { import type {
AccountWithToken, AccountWithToken,
BookmarkInput, BookmarkInput,
@@ -7,6 +5,7 @@ import type {
BookmarkResponse, BookmarkResponse,
} from "./types"; } from "./types";
import { getAuthHeaders } from "./auth"; import { getAuthHeaders } from "./auth";
import { f } from "./fetch";
export function bookmarkMediaToInput( export function bookmarkMediaToInput(
tmdbId: string, tmdbId: string,
@@ -28,12 +27,12 @@ export async function addBookmark(
account: AccountWithToken, account: AccountWithToken,
input: BookmarkInput, input: BookmarkInput,
) { ) {
return ofetch<BookmarkResponse>( return f<BookmarkResponse>(
`/users/${account.userId}/bookmarks/${input.tmdbId}`, `/users/${account.userId}/bookmarks/${input.tmdbId}`,
{ {
method: "POST", method: "POST",
headers: getAuthHeaders(account.token), headers: getAuthHeaders(account.token),
baseURL: url, baseUrl: url,
body: input, body: input,
}, },
); );
@@ -44,12 +43,9 @@ export async function removeBookmark(
account: AccountWithToken, account: AccountWithToken,
id: string, id: string,
) { ) {
return ofetch<{ tmdbId: string }>( return f<{ tmdbId: string }>(`/users/${account.userId}/bookmarks/${id}`, {
`/users/${account.userId}/bookmarks/${id}`,
{
method: "DELETE", method: "DELETE",
headers: getAuthHeaders(account.token), headers: getAuthHeaders(account.token),
baseURL: url, baseUrl: url,
}, });
);
} }

View File

@@ -39,11 +39,7 @@ export function genMnemonic(): string {
return generateMnemonic(wordlist); return generateMnemonic(wordlist);
} }
// eslint-disable-next-line @typescript-eslint/require-await export function signCode(code: string, privateKey: Uint8Array): Uint8Array {
export async function signCode(
code: string,
privateKey: Uint8Array,
): Promise<Uint8Array> {
return forge.pki.ed25519.sign({ return forge.pki.ed25519.sign({
encoding: "utf8", encoding: "utf8",
message: code, message: code,
@@ -62,8 +58,8 @@ export function bytesToBase64Url(bytes: Uint8Array): string {
.replace(/=+$/, ""); .replace(/=+$/, "");
} }
export async function signChallenge(keys: Keys, challengeCode: string) { export function signChallenge(keys: Keys, challengeCode: string) {
const signature = await signCode(challengeCode, keys.privateKey); const signature = signCode(challengeCode, keys.privateKey);
return bytesToBase64Url(signature); return bytesToBase64Url(signature);
} }

53
packages/api/src/fetch.ts Normal file
View File

@@ -0,0 +1,53 @@
export interface FetcherOptions {
baseUrl?: string;
headers?: Record<string, string>;
query?: Record<string, string>;
method?: "HEAD" | "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
readHeaders?: string[];
body?: Record<string, any>;
}
export type FullUrlOptions = Pick<FetcherOptions, "query" | "baseUrl">;
export function makeFullUrl(url: string, ops?: FullUrlOptions): string {
// glue baseUrl and rest of url together
let leftSide = ops?.baseUrl ?? "";
let rightSide = url;
// left side should always end with slash, if its set
if (leftSide.length > 0 && !leftSide.endsWith("/")) leftSide += "/";
// right side should never start with slash
if (rightSide.startsWith("/")) rightSide = rightSide.slice(1);
const fullUrl = leftSide + rightSide;
if (!fullUrl.startsWith("http://") && !fullUrl.startsWith("https://"))
throw new Error(
`Invald URL -- URL doesn't start with a http scheme: '${fullUrl}'`,
);
const parsedUrl = new URL(fullUrl);
Object.entries(ops?.query ?? {}).forEach(([k, v]) => {
parsedUrl.searchParams.set(k, v);
});
return parsedUrl.toString();
}
export async function f<T>(url: string, ops?: FetcherOptions): Promise<T> {
const fullUrl = makeFullUrl(url, ops);
const response = await fetch(fullUrl, {
method: ops?.method ?? "GET",
headers: ops?.headers,
body: ops?.body ? JSON.stringify(ops.body) : undefined,
});
if (!response.ok) {
throw new Error(
`Failed to fetch '${fullUrl}' -- ${response.status} ${response.statusText}`,
);
}
const data = (await response.json()) as T;
return data;
}

View File

@@ -1,17 +1,16 @@
import { ofetch } from "ofetch";
import type { AccountWithToken, BookmarkInput, ProgressInput } from "./types"; import type { AccountWithToken, BookmarkInput, ProgressInput } from "./types";
import { getAuthHeaders } from "./auth"; import { getAuthHeaders } from "./auth";
import { f } from "./fetch";
export function importProgress( export function importProgress(
url: string, url: string,
account: AccountWithToken, account: AccountWithToken,
progressItems: ProgressInput[], progressItems: ProgressInput[],
) { ) {
return ofetch<void>(`/users/${account.userId}/progress/import`, { return f<void>(`/users/${account.userId}/progress/import`, {
method: "PUT", method: "PUT",
body: progressItems, body: progressItems,
baseURL: url, baseUrl: url,
headers: getAuthHeaders(account.token), headers: getAuthHeaders(account.token),
}); });
} }
@@ -21,10 +20,10 @@ export function importBookmarks(
account: AccountWithToken, account: AccountWithToken,
bookmarks: BookmarkInput[], bookmarks: BookmarkInput[],
) { ) {
return ofetch<void>(`/users/${account.userId}/bookmarks`, { return f<void>(`/users/${account.userId}/bookmarks`, {
method: "PUT", method: "PUT",
body: bookmarks, body: bookmarks,
baseURL: url, baseUrl: url,
headers: getAuthHeaders(account.token), headers: getAuthHeaders(account.token),
}); });
} }

View File

@@ -1,21 +1,20 @@
import { ofetch } from "ofetch";
import type { import type {
ChallengeTokenResponse, ChallengeTokenResponse,
LoginInput, LoginInput,
LoginResponse, LoginResponse,
} from "./types"; } from "./types";
import { f } from "./fetch";
export async function getLoginChallengeToken( export async function getLoginChallengeToken(
url: string, url: string,
publicKey: string, publicKey: string,
): Promise<ChallengeTokenResponse> { ): Promise<ChallengeTokenResponse> {
return ofetch<ChallengeTokenResponse>("/auth/login/start", { return f<ChallengeTokenResponse>("/auth/login/start", {
method: "POST", method: "POST",
body: { body: {
publicKey, publicKey,
}, },
baseURL: url, baseUrl: url,
}); });
} }
@@ -23,12 +22,12 @@ export async function loginAccount(
url: string, url: string,
data: LoginInput, data: LoginInput,
): Promise<LoginResponse> { ): Promise<LoginResponse> {
return ofetch<LoginResponse>("/auth/login/complete", { return f<LoginResponse>("/auth/login/complete", {
method: "POST", method: "POST",
body: { body: {
namespace: "movie-web", namespace: "movie-web",
...data, ...data,
}, },
baseURL: url, baseUrl: url,
}); });
} }

View File

@@ -1,9 +1,8 @@
import { ofetch } from "ofetch";
import type { MetaResponse } from "./types"; import type { MetaResponse } from "./types";
import { f } from "./fetch";
export async function getBackendMeta(url: string): Promise<MetaResponse> { export function getBackendMeta(url: string): Promise<MetaResponse> {
return ofetch<MetaResponse>("/meta", { return f<MetaResponse>("/meta", {
baseURL: url, baseUrl: url,
}); });
} }

View File

@@ -1,5 +1,3 @@
import { ofetch } from "ofetch";
import type { import type {
AccountWithToken, AccountWithToken,
ProgressInput, ProgressInput,
@@ -8,6 +6,7 @@ import type {
ProgressUpdateItem, ProgressUpdateItem,
} from "./types"; } from "./types";
import { getAuthHeaders } from "./auth"; import { getAuthHeaders } from "./auth";
import { f } from "./fetch";
export function progressUpdateItemToInput( export function progressUpdateItemToInput(
item: ProgressUpdateItem, item: ProgressUpdateItem,
@@ -72,12 +71,12 @@ export async function setProgress(
account: AccountWithToken, account: AccountWithToken,
input: ProgressInput, input: ProgressInput,
) { ) {
return ofetch<ProgressResponse>( return f<ProgressResponse>(
`/users/${account.userId}/progress/${input.tmdbId}`, `/users/${account.userId}/progress/${input.tmdbId}`,
{ {
method: "PUT", method: "PUT",
headers: getAuthHeaders(account.token), headers: getAuthHeaders(account.token),
baseURL: url, baseUrl: url,
body: input, body: input,
}, },
); );
@@ -90,10 +89,10 @@ export async function removeProgress(
episodeId?: string, episodeId?: string,
seasonId?: string, seasonId?: string,
) { ) {
await ofetch(`/users/${account.userId}/progress/${id}`, { await f(`/users/${account.userId}/progress/${id}`, {
method: "DELETE", method: "DELETE",
headers: getAuthHeaders(account.token), headers: getAuthHeaders(account.token),
baseURL: url, baseUrl: url,
body: { body: {
episodeId, episodeId,
seasonId, seasonId,

View File

@@ -1,22 +1,21 @@
import { ofetch } from "ofetch";
import type { import type {
ChallengeTokenResponse, ChallengeTokenResponse,
RegisterInput, RegisterInput,
SessionResponse, SessionResponse,
UserResponse, UserResponse,
} from "./types"; } from "./types";
import { f } from "./fetch";
export async function getRegisterChallengeToken( export async function getRegisterChallengeToken(
url: string, url: string,
captchaToken?: string, captchaToken?: string,
): Promise<ChallengeTokenResponse> { ): Promise<ChallengeTokenResponse> {
return ofetch<ChallengeTokenResponse>("/auth/register/start", { return f<ChallengeTokenResponse>("/auth/register/start", {
method: "POST", method: "POST",
body: { body: {
captchaToken, captchaToken,
}, },
baseURL: url, baseUrl: url,
}); });
} }
@@ -30,12 +29,12 @@ export async function registerAccount(
url: string, url: string,
data: RegisterInput, data: RegisterInput,
): Promise<RegisterResponse> { ): Promise<RegisterResponse> {
return ofetch<RegisterResponse>("/auth/register/complete", { return f<RegisterResponse>("/auth/register/complete", {
method: "POST", method: "POST",
body: { body: {
namespace: "movie-web", namespace: "movie-web",
...data, ...data,
}, },
baseURL: url, baseUrl: url,
}); });
} }

View File

@@ -1,12 +1,11 @@
import { ofetch } from "ofetch";
import type { AccountWithToken, SessionResponse, SessionUpdate } from "./types"; import type { AccountWithToken, SessionResponse, SessionUpdate } from "./types";
import { getAuthHeaders } from "./auth"; import { getAuthHeaders } from "./auth";
import { f } from "./fetch";
export async function getSessions(url: string, account: AccountWithToken) { export async function getSessions(url: string, account: AccountWithToken) {
return ofetch<SessionResponse[]>(`/users/${account.userId}/sessions`, { return f<SessionResponse[]>(`/users/${account.userId}/sessions`, {
headers: getAuthHeaders(account.token), headers: getAuthHeaders(account.token),
baseURL: url, baseUrl: url,
}); });
} }
@@ -15,11 +14,11 @@ export async function updateSession(
account: AccountWithToken, account: AccountWithToken,
update: SessionUpdate, update: SessionUpdate,
) { ) {
return ofetch<SessionResponse[]>(`/sessions/${account.sessionId}`, { return f<SessionResponse[]>(`/sessions/${account.sessionId}`, {
method: "PATCH", method: "PATCH",
headers: getAuthHeaders(account.token), headers: getAuthHeaders(account.token),
body: update, body: update,
baseURL: url, baseUrl: url,
}); });
} }
@@ -28,9 +27,9 @@ export async function removeSession(
token: string, token: string,
sessionId: string, sessionId: string,
) { ) {
return ofetch<SessionResponse[]>(`/sessions/${sessionId}`, { return f<SessionResponse[]>(`/sessions/${sessionId}`, {
method: "DELETE", method: "DELETE",
headers: getAuthHeaders(token), headers: getAuthHeaders(token),
baseURL: url, baseUrl: url,
}); });
} }

View File

@@ -1,29 +1,28 @@
import { ofetch } from "ofetch";
import type { import type {
AccountWithToken, AccountWithToken,
SettingsInput, SettingsInput,
SettingsResponse, SettingsResponse,
} from "./types"; } from "./types";
import { getAuthHeaders } from "./auth"; import { getAuthHeaders } from "./auth";
import { f } from "./fetch";
export function updateSettings( export function updateSettings(
url: string, url: string,
account: AccountWithToken, account: AccountWithToken,
settings: SettingsInput, settings: SettingsInput,
) { ) {
return ofetch<SettingsResponse>(`/users/${account.userId}/settings`, { return f<SettingsResponse>(`/users/${account.userId}/settings`, {
method: "PUT", method: "PUT",
body: settings, body: settings,
baseURL: url, baseUrl: url,
headers: getAuthHeaders(account.token), headers: getAuthHeaders(account.token),
}); });
} }
export function getSettings(url: string, account: AccountWithToken) { export function getSettings(url: string, account: AccountWithToken) {
return ofetch<SettingsResponse>(`/users/${account.userId}/settings`, { return f<SettingsResponse>(`/users/${account.userId}/settings`, {
method: "GET", method: "GET",
baseURL: url, baseUrl: url,
headers: getAuthHeaders(account.token), headers: getAuthHeaders(account.token),
}); });
} }

View File

@@ -1,5 +1,3 @@
import { ofetch } from "ofetch";
import type { import type {
AccountWithToken, AccountWithToken,
BookmarkMediaItem, BookmarkMediaItem,
@@ -11,6 +9,7 @@ import type {
UserResponse, UserResponse,
} from "./types"; } from "./types";
import { getAuthHeaders } from "./auth"; import { getAuthHeaders } from "./auth";
import { f } from "./fetch";
export function bookmarkResponsesToEntries(responses: BookmarkResponse[]) { export function bookmarkResponsesToEntries(responses: BookmarkResponse[]) {
const entries = responses.map((bookmark) => { const entries = responses.map((bookmark) => {
@@ -83,13 +82,10 @@ export async function getUser(
url: string, url: string,
token: string, token: string,
): Promise<{ user: UserResponse; session: SessionResponse }> { ): Promise<{ user: UserResponse; session: SessionResponse }> {
return ofetch<{ user: UserResponse; session: SessionResponse }>( return f<{ user: UserResponse; session: SessionResponse }>("/users/@me", {
"/users/@me",
{
headers: getAuthHeaders(token), headers: getAuthHeaders(token),
baseURL: url, baseUrl: url,
}, });
);
} }
export async function editUser( export async function editUser(
@@ -97,13 +93,13 @@ export async function editUser(
account: AccountWithToken, account: AccountWithToken,
object: UserEdit, object: UserEdit,
): Promise<{ user: UserResponse; session: SessionResponse }> { ): Promise<{ user: UserResponse; session: SessionResponse }> {
return ofetch<{ user: UserResponse; session: SessionResponse }>( return f<{ user: UserResponse; session: SessionResponse }>(
`/users/${account.userId}`, `/users/${account.userId}`,
{ {
method: "PATCH", method: "PATCH",
headers: getAuthHeaders(account.token), headers: getAuthHeaders(account.token),
body: object, body: object,
baseURL: url, baseUrl: url,
}, },
); );
} }
@@ -112,22 +108,22 @@ export async function deleteUser(
url: string, url: string,
account: AccountWithToken, account: AccountWithToken,
): Promise<UserResponse> { ): Promise<UserResponse> {
return ofetch<UserResponse>(`/users/${account.userId}`, { return f<UserResponse>(`/users/${account.userId}`, {
headers: getAuthHeaders(account.token), headers: getAuthHeaders(account.token),
baseURL: url, baseUrl: url,
}); });
} }
export async function getBookmarks(url: string, account: AccountWithToken) { export async function getBookmarks(url: string, account: AccountWithToken) {
return ofetch<BookmarkResponse[]>(`/users/${account.userId}/bookmarks`, { return f<BookmarkResponse[]>(`/users/${account.userId}/bookmarks`, {
headers: getAuthHeaders(account.token), headers: getAuthHeaders(account.token),
baseURL: url, baseUrl: url,
}); });
} }
export async function getProgress(url: string, account: AccountWithToken) { export async function getProgress(url: string, account: AccountWithToken) {
return ofetch<ProgressResponse[]>(`/users/${account.userId}/progress`, { return f<ProgressResponse[]>(`/users/${account.userId}/progress`, {
headers: getAuthHeaders(account.token), headers: getAuthHeaders(account.token),
baseURL: url, baseUrl: url,
}); });
} }