lots of backend changes to slim up the app provider and to beef up the login provider

This commit is contained in:
Violet Caulfield
2024-10-22 10:52:31 -05:00
parent ef0ac869a9
commit 3ecd05c20f
12 changed files with 156 additions and 142 deletions

View File

@@ -25,8 +25,12 @@ export const serverMutation = async (serverUrl: string) => {
return await getSystemApi(api).getPublicSystemInfo();
}
export const mutateServer = async (server: JellifyServer | undefined) => {
return await AsyncStorage.setItem(AsyncStorageKeys.ServerUrl, JSON.stringify(server));
export const mutateServer = async (server?: JellifyServer) => {
if (!_.isUndefined(server))
return await AsyncStorage.setItem(AsyncStorageKeys.ServerUrl, JSON.stringify(server));
return await AsyncStorage.removeItem(AsyncStorageKeys.ServerUrl);
}
export const mutateServerCredentials = async (credentials?: JellyfinCredentials) => {

View File

@@ -3,15 +3,11 @@ import { QueryKeys } from "../enums/query-keys";
import { createApi, createPublicApi } from "./queries/functions/api";
export const usePublicApi = (serverUrl: string) => useQuery({
queryKey: [QueryKeys.PublicApi, serverUrl],
queryFn: ({ queryKey }) => {
return createPublicApi(queryKey[1])
}
})
queryKey: [QueryKeys.PublicApi, { serverUrl }],
queryFn: createPublicApi
});
export const useApi = () => useQuery({
queryKey: [QueryKeys.Api],
queryFn: () => {
return createApi()
}
queryFn: createApi
})

View File

@@ -2,21 +2,32 @@ import { Api } from "@jellyfin/sdk";
import { fetchCredentials } from "./storage";
import { client } from "../../client";
import _ from "lodash";
import { QueryFunctionContext, QueryKey } from "@tanstack/react-query";
/**
* A promise to build an authenticated Jellyfin API client
* @returns A Promise of the authenticated Jellyfin API client or a rejection
*/
export const createApi: () => Promise<Api> = () => new Promise(async (resolve, reject) => {
let credentials = await fetchCredentials();
export function createApi(): Promise<Api> {
return new Promise(async (resolve, reject) => {
let credentials = await fetchCredentials();
if (_.isUndefined(credentials))
reject("No credentials exist for the current user")
console.log("Signing into Jellyfin")
resolve(client.createApi(credentials!.server, credentials!.password));
});
if (_.isUndefined(credentials)) {
console.warn("No credentials exist for user, launching login flow");
return reject("No credentials exist for the current user");
}
console.log("Signing into Jellyfin")
return resolve(client.createApi(credentials!.server, credentials!.password));
});
}
export const createPublicApi: (serverUrl: string) => Api = (serverUrl) => {
return client.createApi(serverUrl);
export function createPublicApi({ queryKey }: QueryFunctionContext): Promise<Api> {
return new Promise((resolve) => {
///@ts-ignore
const [_key, { serverUrl } ] = queryKey;
resolve(client.createApi(serverUrl));
});
}

View File

@@ -4,20 +4,21 @@ import { getItemsApi } from "@jellyfin/sdk/lib/utils/api/items-api";
import _ from "lodash";
export const fetchMusicLibraries : (api: Api) => Promise<BaseItemDto[]> = (api: Api) => new Promise( async (resolve) => {
export function fetchMusicLibraries(api: Api): Promise<BaseItemDto[]> {
return new Promise( async (resolve) => {
console.log("Fetching music libraries from Jellyfin");
let libraries = await getItemsApi(api).getItems();
console.log("Fetching music libraries from Jellyfin");
let libraries = await getItemsApi(api).getItems();
if (_.isUndefined(libraries.data.Items)) {
console.log("No libraries found on Jellyfin");
return Promise.reject("No libraries found on Jellyfin");
}
if (_.isUndefined(libraries.data.Items)) {
console.log("No libraries found on Jellyfin");
return Promise.reject("No libraries found on Jellyfin");
}
let musicLibraries = libraries.data.Items!.filter(library => library.CollectionType == 'music');
let musicLibraries = libraries.data.Items!.filter(library => library.CollectionType == 'music');
console.log(`Found ${musicLibraries.length} music libraries`);
resolve(musicLibraries);
});
console.log(`Found ${musicLibraries.length} music libraries`);
resolve(musicLibraries);
});
}

View File

@@ -9,9 +9,9 @@ import { useApiClientContext } from "../jellyfin-api-provider";
export default function Login(): React.JSX.Element {
const { serverAddress, username, triggerAuth, setTriggerAuth } = useAuthenticationContext();
const { serverAddress, storedServer, changeServer, username, changeUsername, triggerAuth, setTriggerAuth } = useAuthenticationContext();
const { apiClient, username: clientUsername } = useApiClientContext();
const { apiClient } = useApiClientContext();
const Stack = createStackNavigator();
@@ -22,7 +22,7 @@ export default function Login(): React.JSX.Element {
return (
<Stack.Navigator screenOptions={{ headerShown: false }}>
{
(_.isUndefined(serverAddress) || _.isUndefined(apiClient)) ? (
(_.isUndefined(storedServer) || changeServer) ? (
<Stack.Screen
name="ServerAddress"
options={{
@@ -33,7 +33,7 @@ export default function Login(): React.JSX.Element {
/>
) : (
(_.isUndefined(username) || _.isUndefined(clientUsername)) ? (
(_.isUndefined(username) || changeUsername) ? (
<Stack.Screen
name="ServerAuthentication"
options={{

View File

@@ -14,17 +14,11 @@ import { Heading } from "../../helpers/text";
import Input from "../../helpers/input";
import Button from "../../helpers/button";
const http = "http://"
const https = "https://"
export default function ServerAddress(): React.JSX.Element {
const { setServer, refetchApi } = useApiClientContext();
const { serverAddress, setServerAddress } = useAuthenticationContext();
const { serverAddress, setServerAddress, setChangeServer, useHttps, setUseHttps, refetchServer } = useAuthenticationContext();
const [useHttps, setUseHttps] = useState(true)
const theme = useTheme();
const { apiClient } = useApiClientContext();
const useServerMutation = useMutation({
mutationFn: serverMutation,
@@ -45,9 +39,9 @@ export default function ServerAddress(): React.JSX.Element {
startUpComplete: publicSystemInfoResponse.data.StartupWizardCompleted!
}
setServer(jellifyServer);
refetchApi();
return await mutateServer(jellifyServer);
await mutateServer(jellifyServer);
await refetchServer();
setChangeServer(false);
},
onError: async (error: Error) => {
console.error("An error occurred connecting to the Jellyfin instance", error);

View File

@@ -1,21 +1,20 @@
import React, { useEffect } from "react";
import React from "react";
import { useMutation } from "@tanstack/react-query";
import { useApiClientContext } from "../../jellyfin-api-provider";
import _ from "lodash";
import * as Keychain from "react-native-keychain"
import { JellyfinCredentials } from "../../../api/types/jellyfin-credentials";
import { View } from "tamagui";
import { client } from "../../../api/client";
import { useAuthenticationContext } from "../provider";
import { Heading } from "../../helpers/text";
import Button from "../../helpers/button";
import Input from "../../helpers/input";
export default function ServerAuthentication(): React.JSX.Element {
const { username, setUsername, setServerAddress } = useAuthenticationContext();
const { username, setUsername, setChangeUsername, setServerAddress, storedServer } = useAuthenticationContext();
const [password, setPassword] = React.useState<string | undefined>('');
const { apiClient, server, setUsername: setClientUsername } = useApiClientContext();
const { apiClient, refetchApi } = useApiClientContext();
const useApiMutation = useMutation({
mutationFn: async (credentials: JellyfinCredentials) => {
@@ -23,7 +22,7 @@ export default function ServerAuthentication(): React.JSX.Element {
},
onSuccess: async (authResult, credentials) => {
console.log(`Received auth response from ${server!.name}`)
console.log(`Received auth response from ${storedServer!.name}`)
if (_.isUndefined(authResult))
return Promise.reject(new Error("Authentication result was empty"))
@@ -33,15 +32,15 @@ export default function ServerAuthentication(): React.JSX.Element {
if (_.isUndefined(authResult.data.User))
return Promise.reject(new Error("Unable to login"));
console.log(`Successfully signed in to ${server!.name}`)
console.log(`Successfully signed in to ${storedServer!.name}`)
setUsername(credentials.username);
setClientUsername(credentials.username);
return await Keychain.setInternetCredentials(server!.url, credentials.username, (authResult.data.AccessToken as string));
setChangeUsername(false);
await Keychain.setInternetCredentials(storedServer!.url, credentials.username, (authResult.data.AccessToken as string));
return await refetchApi();
},
onError: async (error: Error) => {
console.error("An error occurred connecting to the Jellyfin instance", error);
return Promise.reject(`An error occured signing into ${server!.name}`);
return Promise.reject(`An error occured signing into ${storedServer!.name}`);
}
});
@@ -55,7 +54,7 @@ export default function ServerAuthentication(): React.JSX.Element {
return (
<View marginHorizontal={10} flex={1} justifyContent='center'>
<Heading>
{ `Sign in to ${server?.name ?? "Jellyfin"}`}
{ `Sign in to ${storedServer?.name ?? "Jellyfin"}`}
</Heading>
<Button
onPress={() => {
@@ -81,8 +80,8 @@ export default function ServerAuthentication(): React.JSX.Element {
onPress={() => {
if (!_.isUndefined(username)) {
console.log(`Signing in to ${server!.name}`);
useApiMutation.mutate({ username, password })
console.log(`Signing in to ${storedServer!.name}`);
useApiMutation.mutate({ username, password });
}
}}
>

View File

@@ -21,14 +21,12 @@ export default function ServerLibrary(): React.JSX.Element {
const { setUsername, libraryName, setLibraryName, libraryId, setLibraryId } = useAuthenticationContext();
const { apiClient, setUsername: setClientUsername } = useApiClientContext();
const { apiClient } = useApiClientContext();
const useLibraries = (api: Api) => useQuery({
queryKey: [QueryKeys.Libraries, api],
queryFn: async ({ queryKey }) => {
return await fetchMusicLibraries(queryKey[1] as Api);
}
queryFn: async ({ queryKey }) => await fetchMusicLibraries(queryKey[1] as Api)
});
const { data : libraries, isPending, refetch } = useLibraries(apiClient!);
@@ -37,7 +35,6 @@ export default function ServerLibrary(): React.JSX.Element {
mutationFn: async () => {
setUsername(undefined);
setClientUsername(undefined);
return Promise.resolve();
}

View File

@@ -1,15 +1,26 @@
import React, { createContext, ReactNode, SetStateAction, useContext, useEffect, useState } from "react";
import { useApi } from "../../api/queries";
import { useCredentials, useServer } from "../../api/queries/keychain";
import { Api } from "@jellyfin/sdk";
import _ from "lodash";
import { SharedWebCredentials } from "react-native-keychain";
import { JellifyServer } from "../../types/JellifyServer";
import { QueryObserverResult, RefetchOptions } from "@tanstack/react-query";
import { mutateServer, mutateServerCredentials } from "../../api/mutators/functions/storage";
interface JellyfinAuthenticationContext {
username: string | undefined;
setUsername: React.Dispatch<SetStateAction<string | undefined>>;
changeUsername: boolean;
setChangeUsername: React.Dispatch<SetStateAction<boolean>>;
useHttp: boolean;
setUseHttp: React.Dispatch<SetStateAction<boolean>>;
useHttps: boolean;
setUseHttps: React.Dispatch<SetStateAction<boolean>>;
serverAddress: string | undefined;
setServerAddress: React.Dispatch<SetStateAction<string | undefined>>;
changeServer: boolean;
setChangeServer: React.Dispatch<SetStateAction<boolean>>;
storedServer: JellifyServer | undefined;
refetchServer: (options?: RefetchOptions | undefined) => Promise<QueryObserverResult<JellifyServer, Error>>;
libraryName: string | undefined;
setLibraryName: React.Dispatch<React.SetStateAction<string | undefined>>;
libraryId: string | undefined;
@@ -20,21 +31,25 @@ interface JellyfinAuthenticationContext {
const JellyfinAuthenticationContextInitializer = () => {
const [username, setUsername] = useState<string | undefined>(undefined);
const [changeUsername, setChangeUsername] = useState<boolean>(false);
const [useHttp, setUseHttp] = useState<boolean>(false);
const [useHttps, setUseHttps] = useState<boolean>(true);
const [serverAddress, setServerAddress] = useState<string | undefined>(undefined);
const [changeServer, setChangeServer] = useState<boolean>(false);
const [libraryName, setLibraryName] = useState<string | undefined>(undefined);
const [libraryId, setLibraryId] = useState<string | undefined>(undefined);
const [triggerAuth, setTriggerAuth] = useState<boolean>(true);
const { data: jellyfinServer, isPending: serverPending } = useServer();
// Fetch from storage on init to load non-sensitive fields from previous logins
const { data: storedServer, isPending: serverPending, refetch: refetchServer } = useServer();
const { data: credentials, isPending: credentialsPending } : { data: SharedWebCredentials | undefined, isPending: boolean } = useCredentials();
useEffect(() => {
if (!_.isUndefined(jellyfinServer)) {
setServerAddress(jellyfinServer.url);
if (!_.isUndefined(storedServer)) {
setServerAddress(storedServer.url);
}
if (!_.isUndefined(credentials)) {
@@ -45,11 +60,33 @@ const JellyfinAuthenticationContextInitializer = () => {
credentialsPending
]);
// Remove stored creds if a change is requested
useEffect(() => {
if (changeUsername)
mutateServerCredentials();
if (changeServer)
mutateServer();
}, [
changeUsername,
changeServer
])
return {
username,
setUsername,
changeUsername,
setChangeUsername,
useHttp,
setUseHttp,
useHttps,
setUseHttps,
serverAddress,
setServerAddress,
changeServer,
setChangeServer,
storedServer,
refetchServer,
libraryName,
setLibraryName,
libraryId,
@@ -63,8 +100,18 @@ const JellyfinAuthenticationContext =
createContext<JellyfinAuthenticationContext>({
username: undefined,
setUsername: () => {},
changeUsername: false,
setChangeUsername: () => {},
useHttp: false,
setUseHttp: () => {},
useHttps: true,
setUseHttps: () => {},
serverAddress: undefined,
setServerAddress: () => {},
changeServer: false,
setChangeServer: () => {},
storedServer: undefined,
refetchServer: () => new Promise(() => {}),
libraryName: undefined,
setLibraryName: () => {},
libraryId: undefined,
@@ -80,8 +127,18 @@ export const JellyfinAuthenticationProvider: ({ children }: {
const {
username,
setUsername,
changeUsername,
setChangeUsername,
useHttp,
setUseHttp,
useHttps,
setUseHttps,
serverAddress,
setServerAddress,
changeServer,
setChangeServer,
storedServer,
refetchServer,
libraryName,
setLibraryName,
libraryId,
@@ -94,8 +151,18 @@ export const JellyfinAuthenticationProvider: ({ children }: {
<JellyfinAuthenticationContext.Provider value={{
username,
setUsername,
changeUsername,
setChangeUsername,
useHttp,
setUseHttp,
useHttps,
setUseHttps,
serverAddress,
setServerAddress,
changeServer,
setChangeServer,
storedServer,
refetchServer,
libraryName,
setLibraryName,
libraryId,

View File

@@ -0,0 +1,2 @@
export const http = "http://"
export const https = "https://"

View File

@@ -32,11 +32,11 @@ function conditionalHomeRender(): React.JSX.Element {
const isDarkMode = useColorScheme() === 'dark';
// If library ID hasn't been set, we haven't completed the auth flow
const { library } = useApiClientContext();
const { apiClient } = useApiClientContext();
return (
<NavigationContainer theme={isDarkMode ? DarkTheme : DefaultTheme}>
{ !_.isUndefined(library) ? (
{ !_.isUndefined(apiClient) ? (
<Navigation />
) : (
<JellyfinAuthenticationProvider>

View File

@@ -1,106 +1,49 @@
import { Api } from '@jellyfin/sdk';
import React, { createContext, ReactNode, useContext, useEffect, useState } from 'react';
import { useApi, usePublicApi } from '../api/queries';
import React, { createContext, ReactNode, useContext } from 'react';
import { useApi } from '../api/queries';
import _ from 'lodash';
import { JellifyServer } from '../types/JellifyServer';
import { useCredentials, useServer } from '../api/queries/keychain';
import { JellifyLibrary } from '../types/JellifyLibrary';
import { SharedWebCredentials } from 'react-native-keychain';
import { QueryObserverResult, RefetchOptions } from '@tanstack/react-query';
interface JellyfinApiClientContext {
apiClient: Api | undefined;
setApiClient: React.Dispatch<React.SetStateAction<Api | undefined>>;
apiPending: boolean;
refetchApi: (options?: RefetchOptions | undefined) => Promise<QueryObserverResult<Api, Error>>;
server: JellifyServer | undefined;
setServer: React.Dispatch<React.SetStateAction<JellifyServer | undefined>>;
library: JellifyLibrary | undefined;
setLibrary: React.Dispatch<React.SetStateAction<JellifyLibrary | undefined>>;
username: string | undefined;
setUsername: React.Dispatch<React.SetStateAction<string | undefined>>;
}
const JellyfinApiClientContextInitializer = () => {
const [apiClient, setApiClient] = useState<Api | undefined>(undefined);
const [server, setServer] = useState<JellifyServer | undefined>(undefined);
const [library, setLibrary] = useState<JellifyLibrary | undefined>(undefined);
const [username, setUsername] = useState<string | undefined>();
const { data: api, isPending: apiPending, refetch: refetchApi } = useApi();
const { data: jellyfinServer, isPending: serverPending } = useServer();
const { data: credentials, isPending: credentialsPending } : { data: SharedWebCredentials | undefined, isPending: boolean } = useCredentials();
useEffect(() => {
if (!_.isUndefined(api)) {
console.log("Using authenticated API client")
setApiClient(api);
} else {
setApiClient(undefined)
}
setServer(jellyfinServer);
setUsername(credentials?.username ?? undefined)
}, [
apiPending,
credentialsPending,
serverPending,
]);
return {
apiClient,
setApiClient,
api,
apiPending,
refetchApi,
server,
setServer,
library,
setLibrary,
username,
setUsername
};
}
export const JellyfinApiClientContext =
createContext<JellyfinApiClientContext>({
apiClient: undefined,
setApiClient: () => {},
apiPending: true,
refetchApi: () => new Promise(() => {}),
server: undefined,
setServer: () => {},
library: undefined,
setLibrary: () => {},
username: undefined,
setUsername: () => {}
});
export const JellyfinApiClientProvider: ({ children }: {
children: ReactNode;
}) => React.JSX.Element = ({ children }: { children: ReactNode }) => {
const {
apiClient,
setApiClient,
api,
apiPending,
refetchApi,
server,
setServer,
library,
setLibrary,
username,
setUsername
} = JellyfinApiClientContextInitializer();
// Add your logic to check if credentials are stored and initialize the API client here.
return (
<JellyfinApiClientContext.Provider value={{
apiClient,
setApiClient,
apiClient: api,
apiPending,
refetchApi,
server,
setServer,
library,
setLibrary,
username,
setUsername
}}>
{children}
</JellyfinApiClientContext.Provider>