From 6d719874fadfb9d9a32b2f13bbd1a48c13918c08 Mon Sep 17 00:00:00 2001 From: f0x Date: Sun, 11 Sep 2022 19:05:06 +0200 Subject: [PATCH] oauth api revocation handling --- web/source/settings-panel/index.js | 2 +- web/source/settings-panel/lib/api/index.js | 98 ++++++++++--------- web/source/settings-panel/lib/api/oauth.js | 25 ++--- web/source/settings-panel/lib/api/user.js | 37 +++++++ .../settings-panel/lib/generate-views.js | 1 - web/source/settings-panel/redux/index.js | 1 + .../settings-panel/redux/reducers/user.js | 32 ++++++ web/source/settings-panel/user/index.js | 21 +--- web/source/settings-panel/user/profile.js | 5 +- 9 files changed, 136 insertions(+), 86 deletions(-) create mode 100644 web/source/settings-panel/lib/api/user.js create mode 100644 web/source/settings-panel/redux/reducers/user.js diff --git a/web/source/settings-panel/index.js b/web/source/settings-panel/index.js index 906163eb9..d59dc7237 100644 --- a/web/source/settings-panel/index.js +++ b/web/source/settings-panel/index.js @@ -79,7 +79,7 @@ function App() { }).then(() => { // Check currently stored auth token for validity if available if (loginState == "callback" || loginState == "login") { - return dispatch(api.oauth.verify()); + return dispatch(api.user.fetchAccount()); } }).then(() => { setTokenChecked(true); diff --git a/web/source/settings-panel/lib/api/index.js b/web/source/settings-panel/lib/api/index.js index f6e826e49..840cbca10 100644 --- a/web/source/settings-panel/lib/api/index.js +++ b/web/source/settings-panel/lib/api/index.js @@ -22,51 +22,58 @@ const Promise = require("bluebird"); const { APIError } = require("../errors"); const { setInstanceInfo } = require("../../redux/reducers/instances").actions; +const oauth = require("../../redux/reducers/oauth").actions; -function apiCall(state, method, route, payload) { - let base = state.oauth.instance; - let auth = state.oauth.token; - console.log(method, base, route, auth); - - return Promise.try(() => { - let url = new URL(base); - url.pathname = route; - let body; - - if (payload != undefined) { - body = JSON.stringify(payload); - } - - let headers = { - "Accept": "application/json", - "Content-Type": "application/json" - }; - - if (auth != undefined) { - headers["Authorization"] = auth; - } - - return fetch(url.toString(), { - method, - headers, - body +function apiCall(method, route, payload) { + return function (dispatch, getState) { + const state = getState(); + let base = state.oauth.instance; + let auth = state.oauth.token; + console.log(method, base, route, "auth:", auth != undefined); + + return Promise.try(() => { + let url = new URL(base); + url.pathname = route; + let body; + + if (payload != undefined) { + body = JSON.stringify(payload); + } + + let headers = { + "Accept": "application/json", + "Content-Type": "application/json" + }; + + if (auth != undefined) { + headers["Authorization"] = auth; + } + + return fetch(url.toString(), { + method, + headers, + body + }); + }).then((res) => { + // try parse json even with error + let json = res.json().catch((e) => { + throw new APIError(`JSON parsing error: ${e.message}`); + }); + + return Promise.all([res, json]); + }).then(([res, json]) => { + if (!res.ok) { + if (auth != undefined && res.status == 401) { + // stored access token is invalid + dispatch(oauth.remove()); + throw new APIError("Stored OAUTH login was no longer valid, please log in again."); + } + throw new APIError(json.error, {json}); + } else { + return json; + } }); - }).then((res) => { - let ok = res.ok; - - // try parse json even with error - let json = res.json().catch((e) => { - throw new APIError(`JSON parsing error: ${e.message}`); - }); - - return Promise.all([ok, json]); - }).then(([ok, json]) => { - if (!ok) { - throw new APIError(json.error, {json}); - } else { - return json; - } - }); + }; } function getCurrentUrl() { @@ -88,7 +95,7 @@ function fetchInstance(domain) { oauth: {instance: domain} }; - return apiCall(fakeState, "GET", "/api/v1/instance"); + return apiCall("GET", "/api/v1/instance")(dispatch, () => fakeState); }).then((json) => { if (json && json.uri) { // TODO: validate instance json more? dispatch(setInstanceInfo([json.uri, json])); @@ -102,5 +109,6 @@ module.exports = { instance: { fetch: fetchInstance }, - oauth: require("./oauth")({apiCall, getCurrentUrl}) + oauth: require("./oauth")({apiCall, getCurrentUrl}), + user: require("./user")({apiCall}) }; \ No newline at end of file diff --git a/web/source/settings-panel/lib/api/oauth.js b/web/source/settings-panel/lib/api/oauth.js index 0fbf236d7..1b985e7bd 100644 --- a/web/source/settings-panel/lib/api/oauth.js +++ b/web/source/settings-panel/lib/api/oauth.js @@ -24,19 +24,20 @@ const { OAUTHError } = require("../errors"); const oauth = require("../../redux/reducers/oauth").actions; const temporary = require("../../redux/reducers/temporary").actions; +const user = require("../../redux/reducers/user").actions; module.exports = function oauthAPI({apiCall, getCurrentUrl}) { return { register: function register(scopes = []) { - return function (dispatch, getState) { + return function (dispatch, _getState) { return Promise.try(() => { - return apiCall(getState(), "POST", "/api/v1/apps", { + return dispatch(apiCall("POST", "/api/v1/apps", { client_name: "GoToSocial Settings", scopes: scopes.join(" "), redirect_uris: getCurrentUrl(), website: getCurrentUrl() - }); + })); }).then((json) => { json.scopes = scopes; dispatch(oauth.setRegistration(json)); @@ -73,13 +74,13 @@ module.exports = function oauthAPI({apiCall, getCurrentUrl}) { throw new OAUTHError("Callback code present, but no client registration is available from localStorage. \nNote: localStorage is unavailable in Private Browsing."); } - return apiCall(getState(), "POST", "/oauth/token", { + return dispatch(apiCall("POST", "/oauth/token", { client_id: reg.client_id, client_secret: reg.client_secret, redirect_uri: getCurrentUrl(), grant_type: "authorization_code", code: code - }); + })); }).then((json) => { console.log(json); window.history.replaceState({}, document.title, window.location.pathname); @@ -88,20 +89,6 @@ module.exports = function oauthAPI({apiCall, getCurrentUrl}) { }; }, - verify: function verify() { - return function (dispatch, getState) { - console.log(getState()); - return Promise.try(() => { - return apiCall(getState(), "GET", "/api/v1/accounts/verify_credentials"); - }).then((account) => { - console.log(account); - }).catch((e) => { - dispatch(oauth.remove()); - throw e; - }); - }; - }, - logout: function logout() { return function (dispatch, _getState) { // TODO: GoToSocial does not have a logout API route yet diff --git a/web/source/settings-panel/lib/api/user.js b/web/source/settings-panel/lib/api/user.js new file mode 100644 index 000000000..e0a9f7dda --- /dev/null +++ b/web/source/settings-panel/lib/api/user.js @@ -0,0 +1,37 @@ +/* + GoToSocial + Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +"use strict"; + +const Promise = require("bluebird"); + +const user = require("../../redux/reducers/user").actions; + +module.exports = function({apiCall}) { + return { + fetchAccount: function fetchAccount() { + return function (dispatch, _getState) { + return Promise.try(() => { + return dispatch(apiCall("GET", "/api/v1/accounts/verify_credentials")); + }).then((account) => { + return dispatch(user.setAccount(account)); + }); + }; + } + }; +}; \ No newline at end of file diff --git a/web/source/settings-panel/lib/generate-views.js b/web/source/settings-panel/lib/generate-views.js index 24822a735..2e825c63a 100644 --- a/web/source/settings-panel/lib/generate-views.js +++ b/web/source/settings-panel/lib/generate-views.js @@ -48,7 +48,6 @@ module.exports = function generateViews(struct) { firstRoute = `${base}/${urlSafe(name)}`; } - console.log(name, ViewComponent); routes.push(( {}}> diff --git a/web/source/settings-panel/redux/index.js b/web/source/settings-panel/redux/index.js index 33e9e5044..ab2694223 100644 --- a/web/source/settings-panel/redux/index.js +++ b/web/source/settings-panel/redux/index.js @@ -35,6 +35,7 @@ const combinedReducers = combineReducers({ oauth: require("./reducers/oauth").reducer, instances: require("./reducers/instances").reducer, temporary: require("./reducers/temporary").reducer, + user: require("./reducers/user").reducer, }); const persistedReducer = persistReducer(persistConfig, combinedReducers); diff --git a/web/source/settings-panel/redux/reducers/user.js b/web/source/settings-panel/redux/reducers/user.js new file mode 100644 index 000000000..480751290 --- /dev/null +++ b/web/source/settings-panel/redux/reducers/user.js @@ -0,0 +1,32 @@ +/* + GoToSocial + Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +"use strict"; + +const {createSlice} = require("@reduxjs/toolkit"); + +module.exports = createSlice({ + name: "user", + initialState: { + }, + reducers: { + setAccount: (state, {payload}) => { + state.account = payload; + } + } +}); \ No newline at end of file diff --git a/web/source/settings-panel/user/index.js b/web/source/settings-panel/user/index.js index 7e95beb6b..9c207365b 100644 --- a/web/source/settings-panel/user/index.js +++ b/web/source/settings-panel/user/index.js @@ -20,26 +20,9 @@ const Promise = require("bluebird"); const React = require("react"); -const { Route, Switch } = require("wouter"); - -module.exports = function UserPanel({oauth, routes}) { - // const [account, setAccount] = React.useState({}); - // const [errorMsg, setError] = React.useState(""); - // const [statusMsg, setStatus] = React.useState("Fetching user info"); - - // React.useEffect(() => { - // Promise.try(() => { - // return oauth.apiRequest("/api/v1/accounts/verify_credentials", "GET"); - // }).then((json) => { - // setAccount(json); - // }).catch((e) => { - // setError(e.message); - // setStatus(""); - // }); - // }, [oauth, setAccount, setError, setStatus]); - - // throw new Error("test"); +const { Switch } = require("wouter"); +module.exports = function UserPanel({routes}) { return ( {routes} diff --git a/web/source/settings-panel/user/profile.js b/web/source/settings-panel/user/profile.js index 8ca8e3176..3d0a16b4e 100644 --- a/web/source/settings-panel/user/profile.js +++ b/web/source/settings-panel/user/profile.js @@ -20,11 +20,14 @@ const Promise = require("bluebird"); const React = require("react"); +const Redux = require("react-redux"); const { useErrorHandler } = require("react-error-boundary"); const Submit = require("../components/submit"); -module.exports = function UserProfile({account, oauth}) { +module.exports = function UserProfile() { + const account = Redux.useSelector(state => state.user.account); + const [errorMsg, setError] = React.useState(""); const [statusMsg, setStatus] = React.useState("");