diff --git a/apps/expo/src/app/sync/trust/[url].tsx b/apps/expo/src/app/sync/trust/[url].tsx index ba03c54..9f64eaa 100644 --- a/apps/expo/src/app/sync/trust/[url].tsx +++ b/apps/expo/src/app/sync/trust/[url].tsx @@ -2,29 +2,18 @@ import { Stack, useLocalSearchParams } from "expo-router"; import { useQuery } from "@tanstack/react-query"; import { H4, Paragraph, Text, View } from "tamagui"; +import { getBackendMeta } from "@movie-web/api"; + import ScreenLayout from "~/components/layout/ScreenLayout"; import { MWButton } from "~/components/ui/Button"; 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() { - const { url } = useLocalSearchParams(); + const { url } = useLocalSearchParams<{ url: string }>(); const meta = useQuery({ queryKey: ["backendMeta", url], - queryFn: () => getBackendMeta(url as string), + queryFn: () => getBackendMeta(url), }); return ( @@ -102,7 +91,7 @@ export default function Page() { gap="$4" > I trust this server - Go back + Go back Already have an account?{" "} diff --git a/apps/expo/src/components/ui/Button.tsx b/apps/expo/src/components/ui/Button.tsx index 7399765..c19f48c 100644 --- a/apps/expo/src/components/ui/Button.tsx +++ b/apps/expo/src/components/ui/Button.tsx @@ -4,35 +4,35 @@ export const MWButton = styled(Button, { variants: { type: { primary: { - backgroundColor: "$buttonPrimaryBackground", - color: "$buttonPrimaryText", + backgroundColor: "white", + color: "black", fontWeight: "bold", pressStyle: { - backgroundColor: "$buttonPrimaryBackgroundHover", + backgroundColor: "$silver100", }, }, secondary: { - backgroundColor: "$buttonSecondaryBackground", - color: "$buttonSecondaryText", + backgroundColor: "$ash700", + color: "$silver300", fontWeight: "bold", pressStyle: { - backgroundColor: "$buttonSecondaryBackgroundHover", + backgroundColor: "$ash500", }, }, purple: { - backgroundColor: "$buttonPurpleBackground", + backgroundColor: "$purple500", color: "white", fontWeight: "bold", pressStyle: { - backgroundColor: "$buttonPurpleBackgroundHover", + backgroundColor: "$purple400", }, }, cancel: { - backgroundColor: "$buttonCancelBackground", + backgroundColor: "$ash500", color: "white", fontWeight: "bold", pressStyle: { - backgroundColor: "$buttonCancelBackgroundHover", + backgroundColor: "$ash300", }, }, }, diff --git a/packages/api/package.json b/packages/api/package.json index 075d0c6..d0a27eb 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -32,7 +32,6 @@ "dependencies": { "@noble/hashes": "^1.4.0", "@scure/bip39": "^1.3.0", - "node-forge": "^1.3.1", - "ofetch": "^1.3.4" + "node-forge": "^1.3.1" } } diff --git a/packages/api/src/auth.ts b/packages/api/src/auth.ts index 99b6448..703070e 100644 --- a/packages/api/src/auth.ts +++ b/packages/api/src/auth.ts @@ -1,6 +1,5 @@ -import { ofetch } from "ofetch"; - import type { LoginResponse } from "./types"; +import { f } from "./fetch"; export function getAuthHeaders(token: string): Record { return { @@ -13,12 +12,12 @@ export async function accountLogin( id: string, deviceName: string, ): Promise { - return ofetch("/auth/login", { + return f("/auth/login", { method: "POST", body: { id, device: deviceName, }, - baseURL: url, + baseUrl: url, }); } diff --git a/packages/api/src/bookmarks.ts b/packages/api/src/bookmarks.ts index b9194b3..4a43844 100644 --- a/packages/api/src/bookmarks.ts +++ b/packages/api/src/bookmarks.ts @@ -1,5 +1,3 @@ -import { ofetch } from "ofetch"; - import type { AccountWithToken, BookmarkInput, @@ -7,6 +5,7 @@ import type { BookmarkResponse, } from "./types"; import { getAuthHeaders } from "./auth"; +import { f } from "./fetch"; export function bookmarkMediaToInput( tmdbId: string, @@ -28,12 +27,12 @@ export async function addBookmark( account: AccountWithToken, input: BookmarkInput, ) { - return ofetch( + return f( `/users/${account.userId}/bookmarks/${input.tmdbId}`, { method: "POST", headers: getAuthHeaders(account.token), - baseURL: url, + baseUrl: url, body: input, }, ); @@ -44,12 +43,9 @@ export async function removeBookmark( account: AccountWithToken, id: string, ) { - return ofetch<{ tmdbId: string }>( - `/users/${account.userId}/bookmarks/${id}`, - { - method: "DELETE", - headers: getAuthHeaders(account.token), - baseURL: url, - }, - ); + return f<{ tmdbId: string }>(`/users/${account.userId}/bookmarks/${id}`, { + method: "DELETE", + headers: getAuthHeaders(account.token), + baseUrl: url, + }); } diff --git a/packages/api/src/crypto.ts b/packages/api/src/crypto.ts index 45d6b41..67f4e2a 100644 --- a/packages/api/src/crypto.ts +++ b/packages/api/src/crypto.ts @@ -39,11 +39,7 @@ export function genMnemonic(): string { return generateMnemonic(wordlist); } -// eslint-disable-next-line @typescript-eslint/require-await -export async function signCode( - code: string, - privateKey: Uint8Array, -): Promise { +export function signCode(code: string, privateKey: Uint8Array): Uint8Array { return forge.pki.ed25519.sign({ encoding: "utf8", message: code, @@ -62,8 +58,8 @@ export function bytesToBase64Url(bytes: Uint8Array): string { .replace(/=+$/, ""); } -export async function signChallenge(keys: Keys, challengeCode: string) { - const signature = await signCode(challengeCode, keys.privateKey); +export function signChallenge(keys: Keys, challengeCode: string) { + const signature = signCode(challengeCode, keys.privateKey); return bytesToBase64Url(signature); } diff --git a/packages/api/src/fetch.ts b/packages/api/src/fetch.ts new file mode 100644 index 0000000..c0260b4 --- /dev/null +++ b/packages/api/src/fetch.ts @@ -0,0 +1,53 @@ +export interface FetcherOptions { + baseUrl?: string; + headers?: Record; + query?: Record; + method?: "HEAD" | "GET" | "POST" | "PUT" | "DELETE" | "PATCH"; + readHeaders?: string[]; + body?: Record; +} + +export type FullUrlOptions = Pick; + +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(url: string, ops?: FetcherOptions): Promise { + 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; +} diff --git a/packages/api/src/import.ts b/packages/api/src/import.ts index 7d6dcca..a63fd9b 100644 --- a/packages/api/src/import.ts +++ b/packages/api/src/import.ts @@ -1,17 +1,16 @@ -import { ofetch } from "ofetch"; - import type { AccountWithToken, BookmarkInput, ProgressInput } from "./types"; import { getAuthHeaders } from "./auth"; +import { f } from "./fetch"; export function importProgress( url: string, account: AccountWithToken, progressItems: ProgressInput[], ) { - return ofetch(`/users/${account.userId}/progress/import`, { + return f(`/users/${account.userId}/progress/import`, { method: "PUT", body: progressItems, - baseURL: url, + baseUrl: url, headers: getAuthHeaders(account.token), }); } @@ -21,10 +20,10 @@ export function importBookmarks( account: AccountWithToken, bookmarks: BookmarkInput[], ) { - return ofetch(`/users/${account.userId}/bookmarks`, { + return f(`/users/${account.userId}/bookmarks`, { method: "PUT", body: bookmarks, - baseURL: url, + baseUrl: url, headers: getAuthHeaders(account.token), }); } diff --git a/packages/api/src/login.ts b/packages/api/src/login.ts index 5408b10..3029eea 100644 --- a/packages/api/src/login.ts +++ b/packages/api/src/login.ts @@ -1,21 +1,20 @@ -import { ofetch } from "ofetch"; - import type { ChallengeTokenResponse, LoginInput, LoginResponse, } from "./types"; +import { f } from "./fetch"; export async function getLoginChallengeToken( url: string, publicKey: string, ): Promise { - return ofetch("/auth/login/start", { + return f("/auth/login/start", { method: "POST", body: { publicKey, }, - baseURL: url, + baseUrl: url, }); } @@ -23,12 +22,12 @@ export async function loginAccount( url: string, data: LoginInput, ): Promise { - return ofetch("/auth/login/complete", { + return f("/auth/login/complete", { method: "POST", body: { namespace: "movie-web", ...data, }, - baseURL: url, + baseUrl: url, }); } diff --git a/packages/api/src/meta.ts b/packages/api/src/meta.ts index b2f8620..cca92e4 100644 --- a/packages/api/src/meta.ts +++ b/packages/api/src/meta.ts @@ -1,9 +1,8 @@ -import { ofetch } from "ofetch"; - import type { MetaResponse } from "./types"; +import { f } from "./fetch"; -export async function getBackendMeta(url: string): Promise { - return ofetch("/meta", { - baseURL: url, +export function getBackendMeta(url: string): Promise { + return f("/meta", { + baseUrl: url, }); } diff --git a/packages/api/src/progress.ts b/packages/api/src/progress.ts index 84fb818..27da773 100644 --- a/packages/api/src/progress.ts +++ b/packages/api/src/progress.ts @@ -1,5 +1,3 @@ -import { ofetch } from "ofetch"; - import type { AccountWithToken, ProgressInput, @@ -8,6 +6,7 @@ import type { ProgressUpdateItem, } from "./types"; import { getAuthHeaders } from "./auth"; +import { f } from "./fetch"; export function progressUpdateItemToInput( item: ProgressUpdateItem, @@ -72,12 +71,12 @@ export async function setProgress( account: AccountWithToken, input: ProgressInput, ) { - return ofetch( + return f( `/users/${account.userId}/progress/${input.tmdbId}`, { method: "PUT", headers: getAuthHeaders(account.token), - baseURL: url, + baseUrl: url, body: input, }, ); @@ -90,10 +89,10 @@ export async function removeProgress( episodeId?: string, seasonId?: string, ) { - await ofetch(`/users/${account.userId}/progress/${id}`, { + await f(`/users/${account.userId}/progress/${id}`, { method: "DELETE", headers: getAuthHeaders(account.token), - baseURL: url, + baseUrl: url, body: { episodeId, seasonId, diff --git a/packages/api/src/register.ts b/packages/api/src/register.ts index d66444c..4ac09bc 100644 --- a/packages/api/src/register.ts +++ b/packages/api/src/register.ts @@ -1,22 +1,21 @@ -import { ofetch } from "ofetch"; - import type { ChallengeTokenResponse, RegisterInput, SessionResponse, UserResponse, } from "./types"; +import { f } from "./fetch"; export async function getRegisterChallengeToken( url: string, captchaToken?: string, ): Promise { - return ofetch("/auth/register/start", { + return f("/auth/register/start", { method: "POST", body: { captchaToken, }, - baseURL: url, + baseUrl: url, }); } @@ -30,12 +29,12 @@ export async function registerAccount( url: string, data: RegisterInput, ): Promise { - return ofetch("/auth/register/complete", { + return f("/auth/register/complete", { method: "POST", body: { namespace: "movie-web", ...data, }, - baseURL: url, + baseUrl: url, }); } diff --git a/packages/api/src/sessions.ts b/packages/api/src/sessions.ts index 79bb9c7..8d996e1 100644 --- a/packages/api/src/sessions.ts +++ b/packages/api/src/sessions.ts @@ -1,12 +1,11 @@ -import { ofetch } from "ofetch"; - import type { AccountWithToken, SessionResponse, SessionUpdate } from "./types"; import { getAuthHeaders } from "./auth"; +import { f } from "./fetch"; export async function getSessions(url: string, account: AccountWithToken) { - return ofetch(`/users/${account.userId}/sessions`, { + return f(`/users/${account.userId}/sessions`, { headers: getAuthHeaders(account.token), - baseURL: url, + baseUrl: url, }); } @@ -15,11 +14,11 @@ export async function updateSession( account: AccountWithToken, update: SessionUpdate, ) { - return ofetch(`/sessions/${account.sessionId}`, { + return f(`/sessions/${account.sessionId}`, { method: "PATCH", headers: getAuthHeaders(account.token), body: update, - baseURL: url, + baseUrl: url, }); } @@ -28,9 +27,9 @@ export async function removeSession( token: string, sessionId: string, ) { - return ofetch(`/sessions/${sessionId}`, { + return f(`/sessions/${sessionId}`, { method: "DELETE", headers: getAuthHeaders(token), - baseURL: url, + baseUrl: url, }); } diff --git a/packages/api/src/settings.ts b/packages/api/src/settings.ts index b28b715..78bcdcd 100644 --- a/packages/api/src/settings.ts +++ b/packages/api/src/settings.ts @@ -1,29 +1,28 @@ -import { ofetch } from "ofetch"; - import type { AccountWithToken, SettingsInput, SettingsResponse, } from "./types"; import { getAuthHeaders } from "./auth"; +import { f } from "./fetch"; export function updateSettings( url: string, account: AccountWithToken, settings: SettingsInput, ) { - return ofetch(`/users/${account.userId}/settings`, { + return f(`/users/${account.userId}/settings`, { method: "PUT", body: settings, - baseURL: url, + baseUrl: url, headers: getAuthHeaders(account.token), }); } export function getSettings(url: string, account: AccountWithToken) { - return ofetch(`/users/${account.userId}/settings`, { + return f(`/users/${account.userId}/settings`, { method: "GET", - baseURL: url, + baseUrl: url, headers: getAuthHeaders(account.token), }); } diff --git a/packages/api/src/user.ts b/packages/api/src/user.ts index 8d2e957..57c7667 100644 --- a/packages/api/src/user.ts +++ b/packages/api/src/user.ts @@ -1,5 +1,3 @@ -import { ofetch } from "ofetch"; - import type { AccountWithToken, BookmarkMediaItem, @@ -11,6 +9,7 @@ import type { UserResponse, } from "./types"; import { getAuthHeaders } from "./auth"; +import { f } from "./fetch"; export function bookmarkResponsesToEntries(responses: BookmarkResponse[]) { const entries = responses.map((bookmark) => { @@ -83,13 +82,10 @@ export async function getUser( url: string, token: string, ): Promise<{ user: UserResponse; session: SessionResponse }> { - return ofetch<{ user: UserResponse; session: SessionResponse }>( - "/users/@me", - { - headers: getAuthHeaders(token), - baseURL: url, - }, - ); + return f<{ user: UserResponse; session: SessionResponse }>("/users/@me", { + headers: getAuthHeaders(token), + baseUrl: url, + }); } export async function editUser( @@ -97,13 +93,13 @@ export async function editUser( account: AccountWithToken, object: UserEdit, ): Promise<{ user: UserResponse; session: SessionResponse }> { - return ofetch<{ user: UserResponse; session: SessionResponse }>( + return f<{ user: UserResponse; session: SessionResponse }>( `/users/${account.userId}`, { method: "PATCH", headers: getAuthHeaders(account.token), body: object, - baseURL: url, + baseUrl: url, }, ); } @@ -112,22 +108,22 @@ export async function deleteUser( url: string, account: AccountWithToken, ): Promise { - return ofetch(`/users/${account.userId}`, { + return f(`/users/${account.userId}`, { headers: getAuthHeaders(account.token), - baseURL: url, + baseUrl: url, }); } export async function getBookmarks(url: string, account: AccountWithToken) { - return ofetch(`/users/${account.userId}/bookmarks`, { + return f(`/users/${account.userId}/bookmarks`, { headers: getAuthHeaders(account.token), - baseURL: url, + baseUrl: url, }); } export async function getProgress(url: string, account: AccountWithToken) { - return ofetch(`/users/${account.userId}/progress`, { + return f(`/users/${account.userId}/progress`, { headers: getAuthHeaders(account.token), - baseURL: url, + baseUrl: url, }); }