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:
@@ -31,8 +31,13 @@ const Login: React.FC = () => {
|
||||
const api = useAtomValue(apiAtom);
|
||||
const navigation = useNavigation();
|
||||
const params = useLocalSearchParams();
|
||||
const { setServer, login, removeServer, initiateQuickConnect } =
|
||||
useJellyfin();
|
||||
const {
|
||||
setServer,
|
||||
login,
|
||||
removeServer,
|
||||
initiateQuickConnect,
|
||||
loginWithSavedCredential,
|
||||
} = useJellyfin();
|
||||
|
||||
const {
|
||||
apiUrl: _apiUrl,
|
||||
@@ -100,7 +105,7 @@ const Login: React.FC = () => {
|
||||
try {
|
||||
const result = CredentialsSchema.safeParse(credentials);
|
||||
if (result.success) {
|
||||
await login(credentials.username, credentials.password);
|
||||
await login(credentials.username, credentials.password, serverName);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
@@ -116,6 +121,10 @@ const Login: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleQuickLoginWithSavedCredential = async (serverUrl: string) => {
|
||||
await loginWithSavedCredential(serverUrl);
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks the availability and validity of a Jellyfin server URL.
|
||||
*
|
||||
@@ -380,9 +389,10 @@ const Login: React.FC = () => {
|
||||
}}
|
||||
/>
|
||||
<PreviousServersList
|
||||
onServerSelect={async (s: any) => {
|
||||
onServerSelect={async (s) => {
|
||||
await handleConnect(s.address);
|
||||
}}
|
||||
onQuickLogin={handleQuickLoginWithSavedCredential}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
@@ -535,6 +545,7 @@ const Login: React.FC = () => {
|
||||
onServerSelect={async (s) => {
|
||||
await handleConnect(s.address);
|
||||
}}
|
||||
onQuickLogin={handleQuickLoginWithSavedCredential}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
3
bun.lock
3
bun.lock
@@ -39,6 +39,7 @@
|
||||
"expo-notifications": "~0.32.15",
|
||||
"expo-router": "~6.0.21",
|
||||
"expo-screen-orientation": "~9.0.8",
|
||||
"expo-secure-store": "^15.0.8",
|
||||
"expo-sensors": "~15.0.8",
|
||||
"expo-sharing": "~14.0.8",
|
||||
"expo-splash-screen": "~31.0.13",
|
||||
@@ -1044,6 +1045,8 @@
|
||||
|
||||
"expo-screen-orientation": ["expo-screen-orientation@9.0.8", "", { "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-qRoPi3E893o3vQHT4h1NKo51+7g2hjRSbDeg1fsSo/u2pOW5s6FCeoacLvD+xofOP33cH2MkE4ua54aWWO7Icw=="],
|
||||
|
||||
"expo-secure-store": ["expo-secure-store@15.0.8", "", { "peerDependencies": { "expo": "*" } }, "sha512-lHnzvRajBu4u+P99+0GEMijQMFCOYpWRO4dWsXSuMt77+THPIGjzNvVKrGSl6mMrLsfVaKL8BpwYZLGlgA+zAw=="],
|
||||
|
||||
"expo-sensors": ["expo-sensors@15.0.8", "", { "dependencies": { "invariant": "^2.2.4" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-ttibOSCYjFAMIfjV+vVukO1v7GKlbcPRfxcRqbTaSMGneewDwVSXbGFImY530fj1BR3mWq4n9jHnuDp8tAEY9g=="],
|
||||
|
||||
"expo-server": ["expo-server@1.0.5", "", {}, "sha512-IGR++flYH70rhLyeXF0Phle56/k4cee87WeQ4mamS+MkVAVP+dDlOHf2nN06Z9Y2KhU0Gp1k+y61KkghF7HdhA=="],
|
||||
|
||||
@@ -1,31 +1,83 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import type React from "react";
|
||||
import { useMemo } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { View } from "react-native";
|
||||
import { ActivityIndicator, Alert, TouchableOpacity, View } from "react-native";
|
||||
import { useMMKVString } from "react-native-mmkv";
|
||||
import { Colors } from "@/constants/Colors";
|
||||
import {
|
||||
deleteServerCredential,
|
||||
type SavedServer,
|
||||
} from "@/utils/secureCredentials";
|
||||
import { ListGroup } from "./list/ListGroup";
|
||||
import { ListItem } from "./list/ListItem";
|
||||
|
||||
interface Server {
|
||||
address: string;
|
||||
}
|
||||
|
||||
interface PreviousServersListProps {
|
||||
onServerSelect: (server: Server) => void;
|
||||
onServerSelect: (server: SavedServer) => void;
|
||||
onQuickLogin?: (serverUrl: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export const PreviousServersList: React.FC<PreviousServersListProps> = ({
|
||||
onServerSelect,
|
||||
onQuickLogin,
|
||||
}) => {
|
||||
const [_previousServers, setPreviousServers] =
|
||||
useMMKVString("previousServers");
|
||||
const [loadingServer, setLoadingServer] = useState<string | null>(null);
|
||||
|
||||
const previousServers = useMemo(() => {
|
||||
return JSON.parse(_previousServers || "[]") as Server[];
|
||||
return JSON.parse(_previousServers || "[]") as SavedServer[];
|
||||
}, [_previousServers]);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleServerPress = async (server: SavedServer) => {
|
||||
if (loadingServer) return; // Prevent double-tap
|
||||
|
||||
if (server.hasCredentials && onQuickLogin) {
|
||||
// Quick login with saved credentials
|
||||
setLoadingServer(server.address);
|
||||
try {
|
||||
await onQuickLogin(server.address);
|
||||
} catch (_error) {
|
||||
// Token expired/invalid, fall back to manual login
|
||||
Alert.alert(
|
||||
t("server.session_expired"),
|
||||
t("server.please_login_again"),
|
||||
[{ text: t("common.ok"), onPress: () => onServerSelect(server) }],
|
||||
);
|
||||
} finally {
|
||||
setLoadingServer(null);
|
||||
}
|
||||
} else {
|
||||
onServerSelect(server);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveCredential = async (serverUrl: string) => {
|
||||
Alert.alert(
|
||||
t("server.remove_saved_login"),
|
||||
t("server.remove_saved_login_description"),
|
||||
[
|
||||
{ text: t("common.cancel"), style: "cancel" },
|
||||
{
|
||||
text: t("common.remove"),
|
||||
style: "destructive",
|
||||
onPress: async () => {
|
||||
await deleteServerCredential(serverUrl);
|
||||
// Update UI
|
||||
const updated = previousServers.map((s) =>
|
||||
s.address === serverUrl
|
||||
? { ...s, hasCredentials: false, username: undefined }
|
||||
: s,
|
||||
);
|
||||
setPreviousServers(JSON.stringify(updated));
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
};
|
||||
|
||||
if (!previousServers.length) return null;
|
||||
|
||||
return (
|
||||
@@ -34,10 +86,33 @@ export const PreviousServersList: React.FC<PreviousServersListProps> = ({
|
||||
{previousServers.map((s) => (
|
||||
<ListItem
|
||||
key={s.address}
|
||||
onPress={() => onServerSelect(s)}
|
||||
title={s.address}
|
||||
showArrow
|
||||
/>
|
||||
onPress={() => handleServerPress(s)}
|
||||
title={s.name || s.address}
|
||||
subtitle={
|
||||
s.hasCredentials
|
||||
? `${s.username} • ${t("server.saved")}`
|
||||
: s.name
|
||||
? s.address
|
||||
: undefined
|
||||
}
|
||||
showArrow={loadingServer !== s.address}
|
||||
disabled={loadingServer === s.address}
|
||||
>
|
||||
{loadingServer === s.address ? (
|
||||
<ActivityIndicator size='small' color={Colors.primary} />
|
||||
) : s.hasCredentials ? (
|
||||
<TouchableOpacity
|
||||
onPress={(e) => {
|
||||
e.stopPropagation();
|
||||
handleRemoveCredential(s.address);
|
||||
}}
|
||||
className='p-1'
|
||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||
>
|
||||
<Ionicons name='key' size={16} color={Colors.primary} />
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
</ListItem>
|
||||
))}
|
||||
<ListItem
|
||||
onPress={() => {
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
"expo-blur": "~15.0.8",
|
||||
"expo-brightness": "~14.0.8",
|
||||
"expo-build-properties": "~1.0.10",
|
||||
"expo-constants": "~18.0.12",
|
||||
"expo-constants": "18.0.12",
|
||||
"expo-dev-client": "~6.0.20",
|
||||
"expo-device": "~8.0.10",
|
||||
"expo-font": "~14.0.10",
|
||||
@@ -58,12 +58,13 @@
|
||||
"expo-notifications": "~0.32.15",
|
||||
"expo-router": "~6.0.21",
|
||||
"expo-screen-orientation": "~9.0.8",
|
||||
"expo-secure-store": "^15.0.8",
|
||||
"expo-sensors": "~15.0.8",
|
||||
"expo-sharing": "~14.0.8",
|
||||
"expo-splash-screen": "~31.0.13",
|
||||
"expo-status-bar": "~3.0.9",
|
||||
"expo-system-ui": "~6.0.9",
|
||||
"expo-task-manager": "~14.0.9",
|
||||
"expo-task-manager": "14.0.9",
|
||||
"expo-web-browser": "~15.0.10",
|
||||
"i18next": "^25.0.0",
|
||||
"jotai": "2.16.0",
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -32,7 +32,12 @@
|
||||
"clear_button": "Clear",
|
||||
"search_for_local_servers": "Search for Local Servers",
|
||||
"searching": "Searching...",
|
||||
"servers": "Servers"
|
||||
"servers": "Servers",
|
||||
"saved": "Saved",
|
||||
"session_expired": "Session Expired",
|
||||
"please_login_again": "Your saved session has expired. Please log in again.",
|
||||
"remove_saved_login": "Remove Saved Login",
|
||||
"remove_saved_login_description": "This will remove your saved credentials for this server. You'll need to enter your username and password again next time."
|
||||
},
|
||||
"home": {
|
||||
"checking_server_connection": "Checking server connection...",
|
||||
@@ -413,7 +418,9 @@
|
||||
"none": "None",
|
||||
"track": "Track",
|
||||
"cancel": "Cancel",
|
||||
"delete": "Delete"
|
||||
"delete": "Delete",
|
||||
"ok": "OK",
|
||||
"remove": "Remove"
|
||||
},
|
||||
"search": {
|
||||
"search": "Search...",
|
||||
|
||||
189
utils/secureCredentials.ts
Normal file
189
utils/secureCredentials.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
import * as SecureStore from "expo-secure-store";
|
||||
import { storage } from "./mmkv";
|
||||
|
||||
const CREDENTIAL_KEY_PREFIX = "credential_";
|
||||
|
||||
export interface ServerCredential {
|
||||
serverUrl: string;
|
||||
serverName: string;
|
||||
token: string;
|
||||
userId: string;
|
||||
username: string;
|
||||
savedAt: number;
|
||||
}
|
||||
|
||||
export interface SavedServer {
|
||||
address: string;
|
||||
name?: string;
|
||||
hasCredentials?: boolean;
|
||||
username?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode server URL to valid secure store key.
|
||||
* Secure store keys must be alphanumeric with underscores.
|
||||
*/
|
||||
export function serverUrlToKey(serverUrl: string): string {
|
||||
// Use base64 encoding, replace non-alphanumeric chars with underscores
|
||||
const encoded = btoa(serverUrl).replace(/[^a-zA-Z0-9]/g, "_");
|
||||
return `${CREDENTIAL_KEY_PREFIX}${encoded}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save credentials for a server to secure storage.
|
||||
*/
|
||||
export async function saveServerCredential(
|
||||
credential: ServerCredential,
|
||||
): Promise<void> {
|
||||
const key = serverUrlToKey(credential.serverUrl);
|
||||
await SecureStore.setItemAsync(key, JSON.stringify(credential));
|
||||
|
||||
// Update previousServers to mark this server as having credentials
|
||||
updatePreviousServerCredentialFlag(
|
||||
credential.serverUrl,
|
||||
true,
|
||||
credential.username,
|
||||
credential.serverName,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve credentials for a server from secure storage.
|
||||
*/
|
||||
export async function getServerCredential(
|
||||
serverUrl: string,
|
||||
): Promise<ServerCredential | null> {
|
||||
const key = serverUrlToKey(serverUrl);
|
||||
const stored = await SecureStore.getItemAsync(key);
|
||||
|
||||
if (stored) {
|
||||
try {
|
||||
return JSON.parse(stored) as ServerCredential;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete credentials for a server from secure storage.
|
||||
*/
|
||||
export async function deleteServerCredential(serverUrl: string): Promise<void> {
|
||||
const key = serverUrlToKey(serverUrl);
|
||||
await SecureStore.deleteItemAsync(key);
|
||||
|
||||
// Update previousServers to mark this server as not having credentials
|
||||
updatePreviousServerCredentialFlag(serverUrl, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if credentials exist for a server (without retrieving them).
|
||||
*/
|
||||
export async function hasServerCredential(serverUrl: string): Promise<boolean> {
|
||||
const key = serverUrlToKey(serverUrl);
|
||||
const stored = await SecureStore.getItemAsync(key);
|
||||
return stored !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all stored credentials for all servers.
|
||||
*/
|
||||
export async function clearAllCredentials(): Promise<void> {
|
||||
const previousServers = getPreviousServers();
|
||||
|
||||
for (const server of previousServers) {
|
||||
await deleteServerCredential(server.address);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to update the previousServers list in MMKV with credential status.
|
||||
*/
|
||||
function updatePreviousServerCredentialFlag(
|
||||
serverUrl: string,
|
||||
hasCredentials: boolean,
|
||||
username?: string,
|
||||
serverName?: string,
|
||||
): void {
|
||||
const previousServers = getPreviousServers();
|
||||
const updatedServers = previousServers.map((server) => {
|
||||
if (server.address === serverUrl) {
|
||||
return {
|
||||
...server,
|
||||
hasCredentials,
|
||||
username: username || server.username,
|
||||
name: serverName || server.name,
|
||||
};
|
||||
}
|
||||
return server;
|
||||
});
|
||||
storage.set("previousServers", JSON.stringify(updatedServers));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get previous servers list from MMKV.
|
||||
*/
|
||||
export function getPreviousServers(): SavedServer[] {
|
||||
const stored = storage.getString("previousServers");
|
||||
if (stored) {
|
||||
try {
|
||||
return JSON.parse(stored) as SavedServer[];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate existing previousServers to new format (add hasCredentials: false).
|
||||
* Should be called on app startup.
|
||||
*/
|
||||
export async function migrateServersList(): Promise<void> {
|
||||
const stored = storage.getString("previousServers");
|
||||
if (!stored) return;
|
||||
|
||||
try {
|
||||
const servers = JSON.parse(stored);
|
||||
// Check if migration needed (old format doesn't have hasCredentials)
|
||||
if (servers.length > 0 && servers[0].hasCredentials === undefined) {
|
||||
const migrated = servers.map((server: SavedServer) => ({
|
||||
address: server.address,
|
||||
name: server.name,
|
||||
hasCredentials: false,
|
||||
username: undefined,
|
||||
}));
|
||||
storage.set("previousServers", JSON.stringify(migrated));
|
||||
}
|
||||
} catch {
|
||||
// If parsing fails, reset to empty array
|
||||
storage.set("previousServers", "[]");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate current session credentials to secure storage.
|
||||
* Should be called on app startup for existing users.
|
||||
*/
|
||||
export async function migrateCurrentSessionToSecureStorage(
|
||||
serverUrl: string,
|
||||
token: string,
|
||||
userId: string,
|
||||
username: string,
|
||||
serverName?: string,
|
||||
): Promise<void> {
|
||||
const existingCredential = await getServerCredential(serverUrl);
|
||||
|
||||
// Only save if not already saved
|
||||
if (!existingCredential) {
|
||||
await saveServerCredential({
|
||||
serverUrl,
|
||||
serverName: serverName || "",
|
||||
token,
|
||||
userId,
|
||||
username,
|
||||
savedAt: Date.now(),
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user