mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-15 15:48:05 +00:00
feat: save login credentials when switching servers
This commit is contained in:
@@ -26,6 +26,13 @@ import { JellyseerrApi, useJellyseerr } from "@/hooks/useJellyseerr";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import { writeErrorLog, writeInfoLog } from "@/utils/log";
|
||||
import { storage } from "@/utils/mmkv";
|
||||
import {
|
||||
deleteServerCredential,
|
||||
getServerCredential,
|
||||
migrateServersList,
|
||||
type SavedServer,
|
||||
saveServerCredential,
|
||||
} from "@/utils/secureCredentials";
|
||||
import { store } from "@/utils/store";
|
||||
|
||||
interface Server {
|
||||
@@ -40,9 +47,15 @@ interface JellyfinContextValue {
|
||||
discoverServers: (url: string) => Promise<Server[]>;
|
||||
setServer: (server: Server) => Promise<void>;
|
||||
removeServer: () => void;
|
||||
login: (username: string, password: string) => Promise<void>;
|
||||
login: (
|
||||
username: string,
|
||||
password: string,
|
||||
serverName?: string,
|
||||
) => Promise<void>;
|
||||
logout: () => Promise<void>;
|
||||
initiateQuickConnect: () => Promise<string | undefined>;
|
||||
loginWithSavedCredential: (serverUrl: string) => Promise<void>;
|
||||
removeSavedCredential: (serverUrl: string) => Promise<void>;
|
||||
}
|
||||
|
||||
const JellyfinContext = createContext<JellyfinContextValue | undefined>(
|
||||
@@ -193,13 +206,24 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
||||
setApi(apiInstance);
|
||||
storage.set("serverUrl", server.address);
|
||||
},
|
||||
onSuccess: (_, server) => {
|
||||
onSuccess: async (_, server) => {
|
||||
const previousServers = JSON.parse(
|
||||
storage.getString("previousServers") || "[]",
|
||||
) as SavedServer[];
|
||||
|
||||
// Check if we have saved credentials for this server
|
||||
const existingServer = previousServers.find(
|
||||
(s) => s.address === server.address,
|
||||
);
|
||||
const updatedServers = [
|
||||
server,
|
||||
...previousServers.filter((s: Server) => s.address !== server.address),
|
||||
|
||||
const updatedServers: SavedServer[] = [
|
||||
{
|
||||
address: server.address,
|
||||
name: existingServer?.name,
|
||||
hasCredentials: existingServer?.hasCredentials ?? false,
|
||||
username: existingServer?.username,
|
||||
},
|
||||
...previousServers.filter((s) => s.address !== server.address),
|
||||
];
|
||||
storage.set(
|
||||
"previousServers",
|
||||
@@ -225,9 +249,11 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
||||
mutationFn: async ({
|
||||
username,
|
||||
password,
|
||||
serverName,
|
||||
}: {
|
||||
username: string;
|
||||
password: string;
|
||||
serverName?: string;
|
||||
}) => {
|
||||
if (!api || !jellyfin) throw new Error("API not initialized");
|
||||
|
||||
@@ -240,6 +266,18 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
||||
setApi(jellyfin.createApi(api?.basePath, auth.data?.AccessToken));
|
||||
storage.set("token", auth.data?.AccessToken);
|
||||
|
||||
// Save credentials to secure storage for quick switching
|
||||
if (api.basePath) {
|
||||
await saveServerCredential({
|
||||
serverUrl: api.basePath,
|
||||
serverName: serverName || "",
|
||||
token: auth.data.AccessToken,
|
||||
userId: auth.data.User.Id || "",
|
||||
username,
|
||||
savedAt: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
const recentPluginSettings = await refreshStreamyfinPluginSettings();
|
||||
if (recentPluginSettings?.jellyseerrServerUrl?.value) {
|
||||
const jellyseerrApi = new JellyseerrApi(
|
||||
@@ -301,12 +339,82 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
||||
setApi(null);
|
||||
setPluginSettings(undefined);
|
||||
await clearAllJellyseerData();
|
||||
// Note: We keep saved credentials for quick switching back
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Logout failed:", error);
|
||||
},
|
||||
});
|
||||
|
||||
const loginWithSavedCredentialMutation = useMutation({
|
||||
mutationFn: async (serverUrl: string) => {
|
||||
if (!jellyfin) throw new Error("Jellyfin not initialized");
|
||||
|
||||
const credential = await getServerCredential(serverUrl);
|
||||
if (!credential) {
|
||||
throw new Error("No saved credential found");
|
||||
}
|
||||
|
||||
// Create API instance with saved token
|
||||
const apiInstance = jellyfin.createApi(serverUrl, credential.token);
|
||||
if (!apiInstance) {
|
||||
throw new Error("Failed to create API instance");
|
||||
}
|
||||
|
||||
// Validate token by fetching current user
|
||||
try {
|
||||
const response = await getUserApi(apiInstance).getCurrentUser();
|
||||
|
||||
// Token is valid, update state
|
||||
setApi(apiInstance);
|
||||
setUser(response.data);
|
||||
storage.set("serverUrl", serverUrl);
|
||||
storage.set("token", credential.token);
|
||||
storage.set("user", JSON.stringify(response.data));
|
||||
|
||||
// Update previousServers list
|
||||
const previousServers = JSON.parse(
|
||||
storage.getString("previousServers") || "[]",
|
||||
) as SavedServer[];
|
||||
const updatedServers: SavedServer[] = [
|
||||
{
|
||||
address: serverUrl,
|
||||
name: credential.serverName,
|
||||
hasCredentials: true,
|
||||
username: credential.username,
|
||||
},
|
||||
...previousServers.filter((s) => s.address !== serverUrl),
|
||||
].slice(0, 5);
|
||||
storage.set("previousServers", JSON.stringify(updatedServers));
|
||||
|
||||
// Refresh plugin settings
|
||||
await refreshStreamyfinPluginSettings();
|
||||
} catch (error) {
|
||||
// Token is invalid/expired - remove it
|
||||
if (
|
||||
axios.isAxiosError(error) &&
|
||||
(error.response?.status === 401 || error.response?.status === 403)
|
||||
) {
|
||||
await deleteServerCredential(serverUrl);
|
||||
throw new Error(t("server.session_expired"));
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Quick login failed:", error);
|
||||
},
|
||||
});
|
||||
|
||||
const removeSavedCredentialMutation = useMutation({
|
||||
mutationFn: async (serverUrl: string) => {
|
||||
await deleteServerCredential(serverUrl);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Failed to remove saved credential:", error);
|
||||
},
|
||||
});
|
||||
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const [initialLoaded, setInitialLoaded] = useState(false);
|
||||
|
||||
@@ -321,6 +429,13 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
||||
if (!jellyfin) return;
|
||||
|
||||
try {
|
||||
// Run migration for server list format (once)
|
||||
const migrated = storage.getBoolean("credentialsMigrated");
|
||||
if (!migrated) {
|
||||
await migrateServersList();
|
||||
storage.set("credentialsMigrated", true);
|
||||
}
|
||||
|
||||
const token = getTokenFromStorage();
|
||||
const serverUrl = getServerUrlFromStorage();
|
||||
const storedUser = getUserFromStorage();
|
||||
@@ -335,6 +450,19 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
||||
|
||||
const response = await getUserApi(apiInstance).getCurrentUser();
|
||||
setUser(response.data);
|
||||
|
||||
// Migrate current session to secure storage if not already saved
|
||||
const existingCredential = await getServerCredential(serverUrl);
|
||||
if (!existingCredential && storedUser?.Name) {
|
||||
await saveServerCredential({
|
||||
serverUrl,
|
||||
serverName: "",
|
||||
token,
|
||||
userId: storedUser.Id || "",
|
||||
username: storedUser.Name,
|
||||
savedAt: Date.now(),
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@@ -350,10 +478,14 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
||||
discoverServers,
|
||||
setServer: (server) => setServerMutation.mutateAsync(server),
|
||||
removeServer: () => removeServerMutation.mutateAsync(),
|
||||
login: (username, password) =>
|
||||
loginMutation.mutateAsync({ username, password }),
|
||||
login: (username, password, serverName) =>
|
||||
loginMutation.mutateAsync({ username, password, serverName }),
|
||||
logout: () => logoutMutation.mutateAsync(),
|
||||
initiateQuickConnect,
|
||||
loginWithSavedCredential: (serverUrl) =>
|
||||
loginWithSavedCredentialMutation.mutateAsync(serverUrl),
|
||||
removeSavedCredential: (serverUrl) =>
|
||||
removeSavedCredentialMutation.mutateAsync(serverUrl),
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user