feat: implement centralized access token management for media downloads and user authentication
This commit is contained in:
95
src/app/utils/auth.ts
Normal file
95
src/app/utils/auth.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* Authentication utilities for managing access tokens across multiple accounts
|
||||
*/
|
||||
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import { getLocalStorageItem, setLocalStorageItem } from '../state/utils/atomWithLocalStorage';
|
||||
import { Session, Sessions, MATRIX_SESSIONS_KEY, CURRENT_SESSION_KEY } from '../state/sessions';
|
||||
|
||||
/**
|
||||
* Global reference to the current MatrixClient instance
|
||||
* Set by initMatrix.ts when a client is created
|
||||
*/
|
||||
let globalMatrixClient: MatrixClient | null = null;
|
||||
|
||||
/**
|
||||
* Set the global MatrixClient reference
|
||||
* This should be called from initMatrix.ts after creating the client
|
||||
* @param client - The MatrixClient instance or null to clear
|
||||
*/
|
||||
export function setGlobalMatrixClient(client: MatrixClient | null): void {
|
||||
globalMatrixClient = client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current MatrixClient instance
|
||||
* @returns The MatrixClient instance or null if not initialized
|
||||
*/
|
||||
export function getGlobalMatrixClient(): MatrixClient | null {
|
||||
return globalMatrixClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the access token for a session in localStorage
|
||||
* This should be called when a token is refreshed by the MatrixClient
|
||||
* @param userId - The user ID
|
||||
* @param newToken - The new access token
|
||||
*/
|
||||
export function updateSessionToken(userId: string, newToken: string): void {
|
||||
const sessions = getLocalStorageItem<Sessions>(MATRIX_SESSIONS_KEY, []);
|
||||
const sessionIndex = sessions.findIndex((s: Session) => s.userId === userId);
|
||||
|
||||
if (sessionIndex >= 0) {
|
||||
sessions[sessionIndex] = {
|
||||
...sessions[sessionIndex],
|
||||
accessToken: newToken,
|
||||
};
|
||||
setLocalStorageItem(MATRIX_SESSIONS_KEY, sessions);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the access token for the currently active session.
|
||||
* Prioritizes the token from the active MatrixClient instance (most up-to-date),
|
||||
* then falls back to localStorage. This ensures we always use the freshest token,
|
||||
* even if it's been refreshed by the SDK.
|
||||
*
|
||||
* @returns The current access token or undefined if no session is active
|
||||
*/
|
||||
export function getCurrentAccessToken(): string | undefined {
|
||||
// First try to get the token from the active MatrixClient if available
|
||||
// This ensures we get the most up-to-date token, including any refreshed tokens
|
||||
if (globalMatrixClient) {
|
||||
const clientToken = globalMatrixClient.getAccessToken?.();
|
||||
if (clientToken) {
|
||||
return clientToken;
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to localStorage if MatrixClient is not available or doesn't have a token
|
||||
const currentUserId = getLocalStorageItem<string | undefined>(CURRENT_SESSION_KEY, undefined);
|
||||
|
||||
if (!currentUserId) {
|
||||
// Fallback to old single-account storage for migration
|
||||
return localStorage.getItem('cinny_access_token') ?? undefined;
|
||||
}
|
||||
|
||||
const sessions = getLocalStorageItem<Sessions>(MATRIX_SESSIONS_KEY, []);
|
||||
const currentSession = sessions.find((s: Session) => s.userId === currentUserId);
|
||||
|
||||
return currentSession?.accessToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the access token for a specific user.
|
||||
* Useful when you need to fetch data for a non-active account.
|
||||
*
|
||||
* @param userId - The Matrix user ID
|
||||
* @returns The access token for the specified user or undefined
|
||||
*/
|
||||
export function getAccessTokenForUser(userId: string): string | undefined {
|
||||
const sessions = getLocalStorageItem<Sessions>(MATRIX_SESSIONS_KEY, []);
|
||||
const session = sessions.find((s: Session) => s.userId === userId);
|
||||
|
||||
return session?.accessToken;
|
||||
}
|
||||
@@ -308,20 +308,28 @@ export const isAuthenticatedMediaUrl = (url: string): boolean =>
|
||||
* For authenticated media URLs, the access token is required.
|
||||
* Falls back to unauthenticated request if authenticated request fails with 401.
|
||||
* Returns an empty blob with error type if all attempts fail, to prevent cascading failures.
|
||||
*
|
||||
* @param src - The media URL to download
|
||||
* @param accessToken - Optional access token (if not provided, will use current session's token)
|
||||
*/
|
||||
export const downloadMedia = async (src: string, accessToken?: string | null): Promise<Blob> => {
|
||||
// Import here to avoid circular dependencies
|
||||
const { getCurrentAccessToken } = await import('./auth');
|
||||
|
||||
// Use provided token, or fall back to current session's token
|
||||
const token = accessToken ?? getCurrentAccessToken();
|
||||
const needsAuth = isAuthenticatedMediaUrl(src);
|
||||
const headers: HeadersInit = {};
|
||||
|
||||
if (needsAuth && accessToken) {
|
||||
headers.Authorization = `Bearer ${accessToken}`;
|
||||
if (needsAuth && token) {
|
||||
headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
try {
|
||||
let res = await fetch(src, { method: 'GET', headers });
|
||||
|
||||
// If we got a 401 and we tried with auth, fallback to unauthenticated request
|
||||
if (!res.ok && res.status === 401 && needsAuth && accessToken) {
|
||||
if (!res.ok && res.status === 401 && needsAuth && token) {
|
||||
console.warn('[downloadMedia] Auth failed (401), attempting unauthenticated fallback for:', src);
|
||||
res = await fetch(src, { method: 'GET' });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user