Add Transmission/NZBGet and per-client paths and much more

Extend multi-download-client support to include Transmission and NZBGet and introduce per-client custom download paths. Adds protocol mapping and new client types, Transmission/NZBGet integration services, API CRUD and validation changes, UI components/modal updates and live path previews, and manager routing by protocol. Includes DB migrations (download_path on download_history, interactive_search_access on users), schema updates, and related processor/service fixes and tests to ensure backward compatibility and proper path resolution.
This commit is contained in:
kikootwo
2026-02-09 19:45:43 -05:00
parent d7acd67aa4
commit 4b90b35748
117 changed files with 9346 additions and 1488 deletions
+35 -1
View File
@@ -8,6 +8,10 @@
import React, { createContext, useContext, useState, useEffect, ReactNode, useRef } from 'react';
import { isTokenExpired, getRefreshTimeMs } from '@/lib/utils/jwt-client';
interface UserPermissions {
interactiveSearch: boolean;
}
interface User {
id: string;
plexId: string;
@@ -16,6 +20,7 @@ interface User {
role: string;
avatarUrl?: string;
authProvider?: string | null; // 'plex' | 'oidc' | 'local' | null
permissions?: UserPermissions;
}
interface AuthContextType {
@@ -73,7 +78,26 @@ export function AuthProvider({ children }: { children: ReactNode }) {
const storedRefreshToken = localStorage.getItem('refreshToken');
if (storedRefreshToken && !isTokenExpired(storedRefreshToken)) {
// Refresh token is still valid, attempt refresh
refreshTokenInternal(storedRefreshToken).finally(() => {
refreshTokenInternal(storedRefreshToken).then(() => {
// Fetch fresh user data from server to pick up role changes,
// avatar updates, etc. - mirrors the non-expired path below.
const currentToken = localStorage.getItem('accessToken');
if (currentToken) {
fetch('/api/auth/me', {
headers: { 'Authorization': `Bearer ${currentToken}` },
})
.then((res) => res.json())
.then((data) => {
if (data.user) {
setUser(data.user);
localStorage.setItem('user', JSON.stringify(data.user));
}
})
.catch((error) => {
console.error('Failed to fetch fresh user data:', error);
});
}
}).finally(() => {
setIsLoading(false);
});
return;
@@ -135,6 +159,16 @@ export function AuthProvider({ children }: { children: ReactNode }) {
setAccessToken(data.accessToken);
localStorage.setItem('accessToken', data.accessToken);
// Restore user state from localStorage if not already in React state.
// This is critical for the mount-time refresh path: when the access
// token has expired but the refresh token is still valid, the mount
// effect calls refreshTokenInternal without ever calling setUser,
// leaving user as null and the app appearing logged-out.
const storedUserData = localStorage.getItem('user');
if (storedUserData) {
setUser(JSON.parse(storedUserData));
}
// Schedule next refresh
scheduleTokenRefresh(data.accessToken);
} else {