mirror of
https://github.com/Sonarr/Sonarr.git
synced 2026-04-19 21:46:43 -04:00
Use hook for OAuth input
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
import BlocklistAppState from './BlocklistAppState';
|
||||
import CaptchaAppState from './CaptchaAppState';
|
||||
import ImportSeriesAppState from './ImportSeriesAppState';
|
||||
import OAuthAppState from './OAuthAppState';
|
||||
import ProviderOptionsAppState from './ProviderOptionsAppState';
|
||||
import SettingsAppState from './SettingsAppState';
|
||||
|
||||
@@ -9,7 +8,6 @@ interface AppState {
|
||||
blocklist: BlocklistAppState;
|
||||
captcha: CaptchaAppState;
|
||||
importSeries: ImportSeriesAppState;
|
||||
oAuth: OAuthAppState;
|
||||
providerOptions: ProviderOptionsAppState;
|
||||
settings: SettingsAppState;
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import { Error } from './AppSectionState';
|
||||
|
||||
interface OAuthAppState {
|
||||
authorizing: boolean;
|
||||
result: Record<string, unknown> | null;
|
||||
error: Error;
|
||||
}
|
||||
|
||||
export default OAuthAppState;
|
||||
@@ -1,17 +1,17 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import { resetOAuth, startOAuth } from 'Store/Actions/oAuthActions';
|
||||
import useOAuth from 'OAuth/useOAuth';
|
||||
import { getValidationFailures } from 'Store/Selectors/selectSettings';
|
||||
import { InputOnChange } from 'typings/inputs';
|
||||
import { useFormInputGroup } from './FormInputGroupContext';
|
||||
|
||||
export interface OAuthInputProps {
|
||||
label?: string;
|
||||
name: string;
|
||||
provider: string;
|
||||
providerData: object;
|
||||
section: string;
|
||||
providerData: Record<string, unknown>;
|
||||
section?: string;
|
||||
onChange: InputOnChange<unknown>;
|
||||
}
|
||||
|
||||
@@ -23,21 +23,17 @@ function OAuthInput({
|
||||
section,
|
||||
onChange,
|
||||
}: OAuthInputProps) {
|
||||
const dispatch = useDispatch();
|
||||
const { authorizing, error, result } = useSelector(
|
||||
(state: AppState) => state.oAuth
|
||||
);
|
||||
const formInputActions = useFormInputGroup();
|
||||
const { authorizing, error, result, startOAuth, resetOAuth } = useOAuth();
|
||||
|
||||
const handlePress = useCallback(() => {
|
||||
dispatch(
|
||||
startOAuth({
|
||||
name,
|
||||
provider,
|
||||
providerData,
|
||||
section,
|
||||
})
|
||||
);
|
||||
}, [name, provider, providerData, section, dispatch]);
|
||||
startOAuth({
|
||||
name,
|
||||
provider,
|
||||
providerData,
|
||||
section,
|
||||
});
|
||||
}, [name, provider, providerData, section, startOAuth]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!result) {
|
||||
@@ -51,9 +47,16 @@ function OAuthInput({
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
dispatch(resetOAuth());
|
||||
resetOAuth();
|
||||
};
|
||||
}, [dispatch]);
|
||||
}, [resetOAuth]);
|
||||
|
||||
useEffect(() => {
|
||||
const validationFailures = getValidationFailures(error);
|
||||
|
||||
formInputActions?.setClientErrors(validationFailures?.errors ?? []);
|
||||
formInputActions?.setClientWarnings(validationFailures?.warnings ?? []);
|
||||
}, [name, error, formInputActions]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { Error } from 'App/State/AppSectionState';
|
||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||
import requestAction from 'Utilities/requestAction';
|
||||
|
||||
const callbackUrl = `${window.location.origin}${window.Sonarr.urlBase}/oauth.html`;
|
||||
|
||||
interface OAuthResult {
|
||||
[key: string]: string | number | boolean;
|
||||
}
|
||||
|
||||
interface OAuthState {
|
||||
authorizing: boolean;
|
||||
result: OAuthResult | null;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
interface StartOAuthParams {
|
||||
name: string;
|
||||
provider?: string;
|
||||
providerData?: Record<string, unknown>;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface OAuthResponse {
|
||||
oauthUrl?: string;
|
||||
poll?: boolean;
|
||||
success?: boolean;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface QueryParams {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
interface WindowWithOAuth extends Window {
|
||||
onCompleteOauth?: (query: string, onComplete: () => void) => void;
|
||||
}
|
||||
|
||||
function showOAuthWindow(
|
||||
url: string,
|
||||
payload: StartOAuthParams,
|
||||
poll = false,
|
||||
ajaxOptions?: Record<string, unknown>
|
||||
): Promise<QueryParams> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const selfWindow = window as WindowWithOAuth;
|
||||
const newWindow = window.open(url);
|
||||
|
||||
if (
|
||||
!newWindow ||
|
||||
newWindow.closed ||
|
||||
typeof newWindow.closed === 'undefined'
|
||||
) {
|
||||
// A fake validation error to mimic a 400 response from the API.
|
||||
const error = Object.assign(
|
||||
new Error('Pop-ups are being blocked by your browser'),
|
||||
{
|
||||
status: 400,
|
||||
responseJSON: [
|
||||
{
|
||||
propertyName: payload.name,
|
||||
errorMessage: 'Pop-ups are being blocked by your browser',
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
|
||||
return reject(error);
|
||||
}
|
||||
|
||||
if (poll) {
|
||||
const pollAction = () => {
|
||||
requestAction({
|
||||
action: 'pollOAuth',
|
||||
queryParams: ajaxOptions,
|
||||
...payload,
|
||||
}).then((response: OAuthResponse) => {
|
||||
if (response.success) {
|
||||
resolve({});
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
pollAction();
|
||||
}, 5000);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
pollAction();
|
||||
}, 5000);
|
||||
} else {
|
||||
selfWindow.onCompleteOauth = function (
|
||||
query: string,
|
||||
onComplete: () => void
|
||||
) {
|
||||
delete selfWindow.onCompleteOauth;
|
||||
|
||||
const queryParams: Record<string, string> = {};
|
||||
const splitQuery = query.substring(1).split('&');
|
||||
|
||||
splitQuery.forEach((param) => {
|
||||
if (param) {
|
||||
const paramSplit = param.split('=');
|
||||
queryParams[paramSplit[0]] = paramSplit[1];
|
||||
}
|
||||
});
|
||||
|
||||
onComplete();
|
||||
resolve(queryParams);
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function executeIntermediateRequest(
|
||||
payload: Record<string, unknown>,
|
||||
ajaxOptions: Record<string, unknown>
|
||||
): Promise<OAuthResponse> {
|
||||
return createAjaxRequest(ajaxOptions).request.then(
|
||||
(data: Record<string, unknown>) => {
|
||||
return requestAction({
|
||||
action: 'continueOAuth',
|
||||
queryParams: {
|
||||
...data,
|
||||
callbackUrl,
|
||||
},
|
||||
...payload,
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const useOAuth = () => {
|
||||
const [oAuthState, setOAuthState] = useState<OAuthState>({
|
||||
authorizing: false,
|
||||
result: null,
|
||||
error: null,
|
||||
});
|
||||
|
||||
const setOAuthValue = useCallback((values: Partial<OAuthState>) => {
|
||||
setOAuthState((prev) => ({ ...prev, ...values }));
|
||||
}, []);
|
||||
|
||||
const resetOAuth = useCallback(() => {
|
||||
setOAuthState({
|
||||
authorizing: false,
|
||||
result: null,
|
||||
error: null,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const startOAuth = useCallback(
|
||||
async (params: StartOAuthParams) => {
|
||||
const { name, ...otherPayload } = params;
|
||||
|
||||
const actionPayload = {
|
||||
action: 'startOAuth',
|
||||
queryParams: { callbackUrl },
|
||||
...otherPayload,
|
||||
};
|
||||
|
||||
setOAuthValue({ authorizing: true });
|
||||
|
||||
try {
|
||||
let startResponse: OAuthResponse = {};
|
||||
|
||||
const response = (await requestAction(actionPayload)) as OAuthResponse;
|
||||
startResponse = response;
|
||||
|
||||
let queryParams: QueryParams | null = null;
|
||||
|
||||
if (response.oauthUrl) {
|
||||
queryParams = await showOAuthWindow(response.oauthUrl, params);
|
||||
} else {
|
||||
const intermediateResponse = await executeIntermediateRequest(
|
||||
otherPayload,
|
||||
response // Pass the entire response as ajaxOptions
|
||||
);
|
||||
startResponse = intermediateResponse;
|
||||
|
||||
if (!intermediateResponse.oauthUrl) {
|
||||
throw new Error('No OAuth URL received from intermediate request');
|
||||
}
|
||||
|
||||
queryParams = await showOAuthWindow(
|
||||
intermediateResponse.oauthUrl,
|
||||
params,
|
||||
intermediateResponse.poll || false,
|
||||
intermediateResponse
|
||||
);
|
||||
}
|
||||
|
||||
const tokenResponse = await requestAction({
|
||||
action: 'getOAuthToken',
|
||||
queryParams: {
|
||||
...startResponse,
|
||||
...queryParams,
|
||||
},
|
||||
...otherPayload,
|
||||
});
|
||||
|
||||
setOAuthValue({
|
||||
authorizing: false,
|
||||
result: tokenResponse,
|
||||
error: null,
|
||||
});
|
||||
|
||||
return tokenResponse;
|
||||
} catch (error) {
|
||||
const oAuthError = error as Error;
|
||||
setOAuthValue({
|
||||
authorizing: false,
|
||||
result: null,
|
||||
error: oAuthError,
|
||||
});
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[setOAuthValue]
|
||||
);
|
||||
|
||||
return {
|
||||
...oAuthState,
|
||||
startOAuth,
|
||||
setOAuthValue,
|
||||
resetOAuth,
|
||||
};
|
||||
};
|
||||
|
||||
export default useOAuth;
|
||||
@@ -1,13 +1,11 @@
|
||||
import * as captcha from './captchaActions';
|
||||
import * as importSeries from './importSeriesActions';
|
||||
import * as oAuth from './oAuthActions';
|
||||
import * as providerOptions from './providerOptionActions';
|
||||
import * as settings from './settingsActions';
|
||||
|
||||
export default [
|
||||
captcha,
|
||||
importSeries,
|
||||
oAuth,
|
||||
providerOptions,
|
||||
settings
|
||||
];
|
||||
|
||||
@@ -1,231 +0,0 @@
|
||||
import $ from 'jquery';
|
||||
import { createAction } from 'redux-actions';
|
||||
import { batchActions } from 'redux-batched-actions';
|
||||
import { set } from 'Store/Actions/baseActions';
|
||||
import { createThunk, handleThunks } from 'Store/thunks';
|
||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||
import requestAction from 'Utilities/requestAction';
|
||||
import getSectionState from 'Utilities/State/getSectionState';
|
||||
import updateSectionState from 'Utilities/State/updateSectionState';
|
||||
import createHandleActions from './Creators/createHandleActions';
|
||||
|
||||
//
|
||||
// Variables
|
||||
|
||||
export const section = 'oAuth';
|
||||
const callbackUrl = `${window.location.origin}${window.Sonarr.urlBase}/oauth.html`;
|
||||
|
||||
//
|
||||
// State
|
||||
|
||||
export const defaultState = {
|
||||
authorizing: false,
|
||||
result: null,
|
||||
error: null
|
||||
};
|
||||
|
||||
//
|
||||
// Actions Types
|
||||
|
||||
export const START_OAUTH = 'oAuth/startOAuth';
|
||||
export const SET_OAUTH_VALUE = 'oAuth/setOAuthValue';
|
||||
export const RESET_OAUTH = 'oAuth/resetOAuth';
|
||||
|
||||
//
|
||||
// Action Creators
|
||||
|
||||
export const startOAuth = createThunk(START_OAUTH);
|
||||
export const setOAuthValue = createAction(SET_OAUTH_VALUE);
|
||||
export const resetOAuth = createAction(RESET_OAUTH);
|
||||
|
||||
//
|
||||
// Helpers
|
||||
|
||||
function showOAuthWindow(url, payload, poll = false, ajaxOptions = undefined) {
|
||||
const deferred = $.Deferred();
|
||||
const selfWindow = window;
|
||||
|
||||
const newWindow = window.open(url);
|
||||
|
||||
if (
|
||||
!newWindow ||
|
||||
newWindow.closed ||
|
||||
typeof newWindow.closed == 'undefined'
|
||||
) {
|
||||
|
||||
// A fake validation error to mimic a 400 response from the API.
|
||||
const error = {
|
||||
status: 400,
|
||||
responseJSON: [
|
||||
{
|
||||
propertyName: payload.name,
|
||||
errorMessage: 'Pop-ups are being blocked by your browser'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
return deferred.reject(error).promise();
|
||||
}
|
||||
|
||||
if (poll) {
|
||||
const pollAction = () => {
|
||||
requestAction({
|
||||
action: 'pollOAuth',
|
||||
queryParams: ajaxOptions,
|
||||
...payload
|
||||
}).then((response) => {
|
||||
if (response.success) {
|
||||
deferred.resolve({});
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
pollAction();
|
||||
}, 5000);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
pollAction();
|
||||
}, 5000);
|
||||
} else {
|
||||
selfWindow.onCompleteOauth = function(query, onComplete) {
|
||||
delete selfWindow.onCompleteOauth;
|
||||
|
||||
const queryParams = {};
|
||||
const splitQuery = query.substring(1).split('&');
|
||||
|
||||
splitQuery.forEach((param) => {
|
||||
if (param) {
|
||||
const paramSplit = param.split('=');
|
||||
|
||||
queryParams[paramSplit[0]] = paramSplit[1];
|
||||
}
|
||||
});
|
||||
|
||||
onComplete();
|
||||
deferred.resolve(queryParams);
|
||||
};
|
||||
}
|
||||
|
||||
return deferred.promise();
|
||||
}
|
||||
|
||||
function executeIntermediateRequest(payload, ajaxOptions) {
|
||||
return createAjaxRequest(ajaxOptions).request.then((data) => {
|
||||
return requestAction({
|
||||
action: 'continueOAuth',
|
||||
queryParams: {
|
||||
...data,
|
||||
callbackUrl
|
||||
},
|
||||
...payload
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Action Handlers
|
||||
|
||||
export const actionHandlers = handleThunks({
|
||||
|
||||
[START_OAUTH]: function(getState, payload, dispatch) {
|
||||
const {
|
||||
name,
|
||||
section: actionSection,
|
||||
...otherPayload
|
||||
} = payload;
|
||||
|
||||
const actionPayload = {
|
||||
action: 'startOAuth',
|
||||
queryParams: { callbackUrl },
|
||||
...otherPayload
|
||||
};
|
||||
|
||||
dispatch(setOAuthValue({
|
||||
authorizing: true
|
||||
}));
|
||||
|
||||
let startResponse = {};
|
||||
|
||||
const promise = requestAction(actionPayload)
|
||||
.then((response) => {
|
||||
startResponse = response;
|
||||
|
||||
if (response.oauthUrl) {
|
||||
return showOAuthWindow(response.oauthUrl, payload);
|
||||
}
|
||||
|
||||
const { poll, ...ajaxOptions } = response;
|
||||
|
||||
return executeIntermediateRequest(otherPayload, ajaxOptions)
|
||||
.then((intermediateResponse) => {
|
||||
startResponse = intermediateResponse;
|
||||
|
||||
return showOAuthWindow(intermediateResponse.oauthUrl, payload, poll, intermediateResponse);
|
||||
});
|
||||
})
|
||||
.then((queryParams) => {
|
||||
return requestAction({
|
||||
action: 'getOAuthToken',
|
||||
queryParams: {
|
||||
...startResponse,
|
||||
...queryParams
|
||||
},
|
||||
...otherPayload
|
||||
});
|
||||
})
|
||||
.then((response) => {
|
||||
dispatch(setOAuthValue({
|
||||
authorizing: false,
|
||||
result: response,
|
||||
error: null
|
||||
}));
|
||||
});
|
||||
|
||||
promise.done(() => {
|
||||
// Clear any previously set save error.
|
||||
dispatch(set({
|
||||
section: actionSection,
|
||||
saveError: null
|
||||
}));
|
||||
});
|
||||
|
||||
promise.fail((xhr) => {
|
||||
const actions = [
|
||||
setOAuthValue({
|
||||
authorizing: false,
|
||||
result: null,
|
||||
error: xhr
|
||||
})
|
||||
];
|
||||
|
||||
if (xhr.status === 400) {
|
||||
// Set a save error so the UI can display validation errors to the user.
|
||||
actions.splice(0, 0, set({
|
||||
section: actionSection,
|
||||
saveError: xhr
|
||||
}));
|
||||
}
|
||||
|
||||
dispatch(batchActions(actions));
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
//
|
||||
// Reducers
|
||||
|
||||
export const reducers = createHandleActions({
|
||||
|
||||
[SET_OAUTH_VALUE]: function(state, { payload }) {
|
||||
const newState = Object.assign(getSectionState(state, section), payload);
|
||||
|
||||
return updateSectionState(state, section, newState);
|
||||
},
|
||||
|
||||
[RESET_OAUTH]: function(state) {
|
||||
return updateSectionState(state, section, defaultState);
|
||||
}
|
||||
|
||||
}, defaultState, section);
|
||||
Reference in New Issue
Block a user