feat: save login credentials when switching servers

This commit is contained in:
Fredrik Burmester
2026-01-05 21:28:00 +01:00
parent 9ca852bb7e
commit 37b0b10098
7 changed files with 445 additions and 27 deletions

View File

@@ -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(() => {