mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-02 20:30:10 +00:00
fix plex home profile login redirect
This commit is contained in:
@@ -265,11 +265,15 @@ function LoginContent() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Poll for authorization
|
// Poll for authorization
|
||||||
await login(pinId);
|
const loginResult = await login(pinId);
|
||||||
|
|
||||||
// Close popup
|
// Close popup
|
||||||
authWindow.close();
|
authWindow.close();
|
||||||
|
|
||||||
|
if (loginResult === 'profile-selection-required') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Redirect to intended page or homepage
|
// Redirect to intended page or homepage
|
||||||
const redirect = searchParams.get('redirect') || '/';
|
const redirect = searchParams.get('redirect') || '/';
|
||||||
router.push(redirect);
|
router.push(redirect);
|
||||||
|
|||||||
@@ -24,11 +24,13 @@ interface User {
|
|||||||
permissions?: UserPermissions;
|
permissions?: UserPermissions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type LoginResult = 'authenticated' | 'profile-selection-required';
|
||||||
|
|
||||||
interface AuthContextType {
|
interface AuthContextType {
|
||||||
user: User | null;
|
user: User | null;
|
||||||
accessToken: string | null;
|
accessToken: string | null;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
login: (pinId: number) => Promise<void>;
|
login: (pinId: number) => Promise<LoginResult>;
|
||||||
logout: () => void;
|
logout: () => void;
|
||||||
refreshToken: () => Promise<void>;
|
refreshToken: () => Promise<void>;
|
||||||
setAuthData: (user: User, accessToken: string) => void;
|
setAuthData: (user: User, accessToken: string) => void;
|
||||||
@@ -182,7 +184,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Poll Plex OAuth callback during login
|
// Poll Plex OAuth callback during login
|
||||||
const login = async (pinId: number) => {
|
const login = async (pinId: number): Promise<LoginResult> => {
|
||||||
const maxAttempts = 60; // 2 minutes total
|
const maxAttempts = 60; // 2 minutes total
|
||||||
let attempts = 0;
|
let attempts = 0;
|
||||||
|
|
||||||
@@ -211,7 +213,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
// Redirect to profile selection page
|
// Redirect to profile selection page
|
||||||
// Note: Plex token is stored server-side for security, not in sessionStorage
|
// Note: Plex token is stored server-side for security, not in sessionStorage
|
||||||
window.location.href = data.redirectUrl;
|
window.location.href = data.redirectUrl;
|
||||||
return;
|
return 'profile-selection-required';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Login successful (no profile selection needed)
|
// Login successful (no profile selection needed)
|
||||||
@@ -226,7 +228,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
// Schedule auto-refresh
|
// Schedule auto-refresh
|
||||||
scheduleTokenRefresh(data.accessToken);
|
scheduleTokenRefresh(data.accessToken);
|
||||||
|
|
||||||
return;
|
return 'authenticated';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Still waiting for authorization
|
// Still waiting for authorization
|
||||||
|
|||||||
@@ -20,13 +20,15 @@ vi.mock('@/lib/utils/jwt-client', () => ({
|
|||||||
|
|
||||||
function TestConsumer() {
|
function TestConsumer() {
|
||||||
const { user, accessToken, isLoading, login, logout, refreshToken, setAuthData } = useAuth();
|
const { user, accessToken, isLoading, login, logout, refreshToken, setAuthData } = useAuth();
|
||||||
|
const [loginResult, setLoginResult] = React.useState('none');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div data-testid="loading">{String(isLoading)}</div>
|
<div data-testid="loading">{String(isLoading)}</div>
|
||||||
<div data-testid="user">{user?.username ?? 'none'}</div>
|
<div data-testid="user">{user?.username ?? 'none'}</div>
|
||||||
<div data-testid="token">{accessToken ?? 'none'}</div>
|
<div data-testid="token">{accessToken ?? 'none'}</div>
|
||||||
<button type="button" onClick={() => void login(123)}>
|
<div data-testid="login-result">{loginResult}</div>
|
||||||
|
<button type="button" onClick={() => void login(123).then(setLoginResult)}>
|
||||||
login
|
login
|
||||||
</button>
|
</button>
|
||||||
<button type="button" onClick={logout}>
|
<button type="button" onClick={logout}>
|
||||||
@@ -188,6 +190,34 @@ describe('AuthProvider', () => {
|
|||||||
expect(screen.getByTestId('token')).toHaveTextContent('login-access');
|
expect(screen.getByTestId('token')).toHaveTextContent('login-access');
|
||||||
expect(localStorage.getItem('accessToken')).toBe('login-access');
|
expect(localStorage.getItem('accessToken')).toBe('login-access');
|
||||||
expect(localStorage.getItem('refreshToken')).toBe('login-refresh');
|
expect(localStorage.getItem('refreshToken')).toBe('login-refresh');
|
||||||
|
expect(screen.getByTestId('login-result')).toHaveTextContent('authenticated');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns profile selection result without storing auth data for Plex Home users', async () => {
|
||||||
|
const fetchMock = vi.fn().mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
json: async () => ({
|
||||||
|
success: true,
|
||||||
|
authorized: true,
|
||||||
|
requiresProfileSelection: true,
|
||||||
|
redirectUrl: '/auth/select-profile?pinId=123',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.stubGlobal('fetch', fetchMock);
|
||||||
|
|
||||||
|
renderAuthProvider();
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'login' }));
|
||||||
|
|
||||||
|
await waitFor(() => expect(screen.getByTestId('login-result')).toHaveTextContent('profile-selection-required'));
|
||||||
|
|
||||||
|
expect(locationStub.href).toBe('/auth/select-profile?pinId=123');
|
||||||
|
expect(screen.getByTestId('user')).toHaveTextContent('none');
|
||||||
|
expect(screen.getByTestId('token')).toHaveTextContent('none');
|
||||||
|
expect(localStorage.getItem('accessToken')).toBeNull();
|
||||||
|
expect(localStorage.getItem('refreshToken')).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('logs out by clearing storage and redirecting to the login page', () => {
|
it('logs out by clearing storage and redirecting to the login page', () => {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ type RenderWithProvidersOptions = Omit<RenderOptions, 'wrapper'> & {
|
|||||||
user: MockUser | null;
|
user: MockUser | null;
|
||||||
accessToken: string | null;
|
accessToken: string | null;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
login: (pinId: number) => Promise<void>;
|
login: (pinId: number) => Promise<'authenticated' | 'profile-selection-required'>;
|
||||||
logout: () => void;
|
logout: () => void;
|
||||||
refreshToken: () => Promise<void>;
|
refreshToken: () => Promise<void>;
|
||||||
setAuthData: (user: MockUser, accessToken: string) => void;
|
setAuthData: (user: MockUser, accessToken: string) => void;
|
||||||
|
|||||||
Reference in New Issue
Block a user