This commit is contained in:
kikootwo
2026-05-15 06:30:53 -04:00
4 changed files with 43 additions and 7 deletions
+5 -1
View File
@@ -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);
+6 -4
View File
@@ -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
+31 -1
View File
@@ -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', () => {
+1 -1
View File
@@ -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;