From dc1d536c3515576f1b259c471e0119c2074b96a3 Mon Sep 17 00:00:00 2001 From: f0x Date: Sun, 11 Sep 2022 18:41:29 +0200 Subject: [PATCH] split oauth api functions --- web/source/settings-panel/index.js | 2 +- web/source/settings-panel/lib/api.js | 196 --------------------- web/source/settings-panel/lib/api/index.js | 106 +++++++++++ web/source/settings-panel/lib/api/oauth.js | 113 ++++++++++++ 4 files changed, 220 insertions(+), 197 deletions(-) delete mode 100644 web/source/settings-panel/lib/api.js create mode 100644 web/source/settings-panel/lib/api/index.js create mode 100644 web/source/settings-panel/lib/api/oauth.js diff --git a/web/source/settings-panel/index.js b/web/source/settings-panel/index.js index dea4ef5d1..906163eb9 100644 --- a/web/source/settings-panel/index.js +++ b/web/source/settings-panel/index.js @@ -73,7 +73,7 @@ function App() { if (code == undefined) { setErrorMsg(new Error("Waiting for OAUTH callback but no ?code= provided. You can try logging in again:")); } else { - return dispatch(api.oauth.fetchToken(code)); + return dispatch(api.oauth.tokenize(code)); } } }).then(() => { diff --git a/web/source/settings-panel/lib/api.js b/web/source/settings-panel/lib/api.js deleted file mode 100644 index ce39c3e4a..000000000 --- a/web/source/settings-panel/lib/api.js +++ /dev/null @@ -1,196 +0,0 @@ -/* - 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 { APIError, OAUTHError } = require("./errors"); -const oauth = require("../redux/reducers/oauth").actions; -const temporary = require("../redux/reducers/temporary").actions; -const { setInstanceInfo } = require("../redux/reducers/instances").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 - }); - }).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() { - return `${window.location.origin}${window.location.pathname}`; -} - -function fetchInstance(domain) { - return function(dispatch, getState) { - return Promise.try(() => { - let lookup = getState().instances.info[domain]; - if (lookup != undefined) { - return lookup; - } - - // apiCall expects to pull the domain from state, - // but we don't want to store it there yet - // so we mock the API here with our function argument - let fakeState = { - oauth: {instance: domain} - }; - - return apiCall(fakeState, "GET", "/api/v1/instance"); - }).then((json) => { - if (json && json.uri) { // TODO: validate instance json more? - dispatch(setInstanceInfo([json.uri, json])); - return json; - } - }); - }; -} - -function fetchRegistration(scopes=[]) { - return function(dispatch, getState) { - return Promise.try(() => { - return apiCall(getState(), "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)); - }); - }; -} - -function startAuthorize() { - return function(dispatch, getState) { - let state = getState(); - let reg = state.oauth.registration; - let base = new URL(state.oauth.instance); - - base.pathname = "/oauth/authorize"; - base.searchParams.set("client_id", reg.client_id); - base.searchParams.set("redirect_uri", getCurrentUrl()); - base.searchParams.set("response_type", "code"); - base.searchParams.set("scope", reg.scopes.join(" ")); - - dispatch(oauth.setLoginState("callback")); - dispatch(temporary.setStatus("Redirecting to instance login...")); - - // send user to instance's login flow - window.location.assign(base.href); - }; -} - -function fetchToken(code) { - return function(dispatch, getState) { - let reg = getState().oauth.registration; - - return Promise.try(() => { - if (reg == undefined || reg.client_id == undefined) { - 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", { - 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); - return dispatch(oauth.login(json)); - }); - }; -} - -function verifyAuth() { - 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; - }); - }; -} - -function oauthLogout() { - return function(dispatch, _getState) { - // TODO: GoToSocial does not have a logout API route yet - - return dispatch(oauth.remove()); - }; -} - -module.exports = { - instance: { - fetch: fetchInstance - }, - oauth: { - register: fetchRegistration, - authorize: startAuthorize, - fetchToken, - verify: verifyAuth, - logout: oauthLogout - } -}; \ No newline at end of file diff --git a/web/source/settings-panel/lib/api/index.js b/web/source/settings-panel/lib/api/index.js new file mode 100644 index 000000000..f6e826e49 --- /dev/null +++ b/web/source/settings-panel/lib/api/index.js @@ -0,0 +1,106 @@ +/* + 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 { APIError } = require("../errors"); +const { setInstanceInfo } = require("../../redux/reducers/instances").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 + }); + }).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() { + return `${window.location.origin}${window.location.pathname}`; +} + +function fetchInstance(domain) { + return function(dispatch, getState) { + return Promise.try(() => { + let lookup = getState().instances.info[domain]; + if (lookup != undefined) { + return lookup; + } + + // apiCall expects to pull the domain from state, + // but we don't want to store it there yet + // so we mock the API here with our function argument + let fakeState = { + oauth: {instance: domain} + }; + + return apiCall(fakeState, "GET", "/api/v1/instance"); + }).then((json) => { + if (json && json.uri) { // TODO: validate instance json more? + dispatch(setInstanceInfo([json.uri, json])); + return json; + } + }); + }; +} + +module.exports = { + instance: { + fetch: fetchInstance + }, + oauth: require("./oauth")({apiCall, getCurrentUrl}) +}; \ 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 new file mode 100644 index 000000000..0fbf236d7 --- /dev/null +++ b/web/source/settings-panel/lib/api/oauth.js @@ -0,0 +1,113 @@ +/* + 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 { OAUTHError } = require("../errors"); + +const oauth = require("../../redux/reducers/oauth").actions; +const temporary = require("../../redux/reducers/temporary").actions; + +module.exports = function oauthAPI({apiCall, getCurrentUrl}) { + return { + + register: function register(scopes = []) { + return function (dispatch, getState) { + return Promise.try(() => { + return apiCall(getState(), "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)); + }); + }; + }, + + authorize: function authorize() { + return function (dispatch, getState) { + let state = getState(); + let reg = state.oauth.registration; + let base = new URL(state.oauth.instance); + + base.pathname = "/oauth/authorize"; + base.searchParams.set("client_id", reg.client_id); + base.searchParams.set("redirect_uri", getCurrentUrl()); + base.searchParams.set("response_type", "code"); + base.searchParams.set("scope", reg.scopes.join(" ")); + + dispatch(oauth.setLoginState("callback")); + dispatch(temporary.setStatus("Redirecting to instance login...")); + + // send user to instance's login flow + window.location.assign(base.href); + }; + }, + + tokenize: function tokenize(code) { + return function (dispatch, getState) { + let reg = getState().oauth.registration; + + return Promise.try(() => { + if (reg == undefined || reg.client_id == undefined) { + 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", { + 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); + return dispatch(oauth.login(json)); + }); + }; + }, + + 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 + + return dispatch(oauth.remove()); + }; + } + }; +}; \ No newline at end of file