diff options
author | 2022-09-29 12:02:41 +0200 | |
---|---|---|
committer | 2022-09-29 12:02:41 +0200 | |
commit | 938328cd077d40b75e0834d56ff8d43ad035fd2b (patch) | |
tree | 76ed59a9adf8a40e83c99a3ea34ce7cb5a5f8877 /web/source/settings-panel/lib | |
parent | [chore] simplify generating log entry caller information (#863) (diff) | |
download | gotosocial-938328cd077d40b75e0834d56ff8d43ad035fd2b.tar.xz |
[frontend] Unified panels (#812)
* settings panel restructuring
* clean up old Gin handlers
* colorscheme redesign, some other small css tweaks
* basic router layout, error boundary
* colorscheme redesign, some other small css tweaks
* kebab-case consistency
* superfluous padding on applist
* remove unused consts
* redux, whitespace changes..
* use .jsx extensions for components
* login flow up till app registration
* full redux oauth implementation, with basic error handling
* split oauth api functions
* oauth api revocation handling
* basic profile change submission
* move old dir
* profile overview
* fix keeping track of the wrong instance url (for different instance/api domains)
* use redux state for profile form
* delete old/index.js, old/basic.js, fully implemented
* implement old/user/profile.js
* implement password change
* remove debug logging
* support future api for removing files
* customize profile css
* remove unneeded wrapper components
* restructure form fields
* start on admin pages
* admin panel settings
* admin settings panel
* remove old/admin files
* add top-level redirect
* refactor/cleanup forms
* only do API checks on logged-in state
* admin-status based routing
* federation block routing
* federation blocks
* upgrade dependencies
* react 18 changes
* media cleanup
* fix useEffect hooks
* remove unused require
* custom emoji base
* emoji uploader
* delete last old panel files
* sidebar styling, remove unused page
* refactor submit functions
* fix sidebar boxshadow-border
* fix old css variables
* fix fake-toot avatar
* fix non-square emoji
* fix user settings redux keys
* properly get admin account contact from instance response
* Account.source default values
* source.status_format key
* mobile responsiveness
* mobile element tweaks
* proper redirect after removing block
* add redirects for old setting panel urls
* deletes
* fix mobile overflow
* clean up debug logging calls
Diffstat (limited to 'web/source/settings-panel/lib')
-rw-r--r-- | web/source/settings-panel/lib/api/admin.js | 192 | ||||
-rw-r--r-- | web/source/settings-panel/lib/api/index.js | 185 | ||||
-rw-r--r-- | web/source/settings-panel/lib/api/oauth.js | 124 | ||||
-rw-r--r-- | web/source/settings-panel/lib/api/user.js | 67 | ||||
-rw-r--r-- | web/source/settings-panel/lib/errors.js | 27 | ||||
-rw-r--r-- | web/source/settings-panel/lib/get-views.js | 102 | ||||
-rw-r--r-- | web/source/settings-panel/lib/panel.js | 134 | ||||
-rw-r--r-- | web/source/settings-panel/lib/submit.js | 48 |
8 files changed, 879 insertions, 0 deletions
diff --git a/web/source/settings-panel/lib/api/admin.js b/web/source/settings-panel/lib/api/admin.js new file mode 100644 index 000000000..1df47b693 --- /dev/null +++ b/web/source/settings-panel/lib/api/admin.js @@ -0,0 +1,192 @@ +/* + 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 <http://www.gnu.org/licenses/>. +*/ + +"use strict"; + +const Promise = require("bluebird"); +const isValidDomain = require("is-valid-domain"); + +const instance = require("../../redux/reducers/instances").actions; +const admin = require("../../redux/reducers/admin").actions; + +module.exports = function ({ apiCall, getChanges }) { + const adminAPI = { + updateInstance: function updateInstance() { + return function (dispatch, getState) { + return Promise.try(() => { + const state = getState().instances.adminSettings; + + const update = getChanges(state, { + formKeys: ["title", "short_description", "description", "contact_account.username", "email", "terms"], + renamedKeys: {"contact_account.username": "contact_username"}, + // fileKeys: ["avatar", "header"] + }); + + return dispatch(apiCall("PATCH", "/api/v1/instance", update, "form")); + }).then((data) => { + return dispatch(instance.setInstanceInfo(data)); + }); + }; + }, + + fetchDomainBlocks: function fetchDomainBlocks() { + return function (dispatch, _getState) { + return Promise.try(() => { + return dispatch(apiCall("GET", "/api/v1/admin/domain_blocks")); + }).then((data) => { + return dispatch(admin.setBlockedInstances(data)); + }); + }; + }, + + updateDomainBlock: function updateDomainBlock(domain) { + return function (dispatch, getState) { + return Promise.try(() => { + const state = getState().admin.newInstanceBlocks[domain]; + const update = getChanges(state, { + formKeys: ["domain", "obfuscate", "public_comment", "private_comment"], + }); + + return dispatch(apiCall("POST", "/api/v1/admin/domain_blocks", update, "form")); + }).then((block) => { + return Promise.all([ + dispatch(admin.newDomainBlock([domain, block])), + dispatch(admin.setDomainBlock([domain, block])) + ]); + }); + }; + }, + + getEditableDomainBlock: function getEditableDomainBlock(domain) { + return function (dispatch, getState) { + let data = getState().admin.blockedInstances[domain]; + return dispatch(admin.newDomainBlock([domain, data])); + }; + }, + + bulkDomainBlock: function bulkDomainBlock() { + return function (dispatch, getState) { + let invalidDomains = []; + let success = 0; + + return Promise.try(() => { + const state = getState().admin.bulkBlock; + let list = state.list; + let domains; + + let fields = getChanges(state, { + formKeys: ["obfuscate", "public_comment", "private_comment"] + }); + + let defaultDate = new Date().toUTCString(); + + if (list[0] == "[") { + domains = JSON.parse(state.list); + } else { + domains = list.split("\n").map((line_) => { + let line = line_.trim(); + if (line.length == 0) { + return null; + } + + if (!isValidDomain(line, {wildcard: true, allowUnicode: true})) { + invalidDomains.push(line); + return null; + } + + return { + domain: line, + created_at: defaultDate, + ...fields + }; + }).filter((a) => a != null); + } + + if (domains.length == 0) { + return; + } + + const update = { + domains: new Blob([JSON.stringify(domains)], {type: "application/json"}) + }; + + return dispatch(apiCall("POST", "/api/v1/admin/domain_blocks?import=true", update, "form")); + }).then((blocks) => { + if (blocks != undefined) { + return Promise.each(blocks, (block) => { + success += 1; + return dispatch(admin.setDomainBlock([block.domain, block])); + }); + } + }).then(() => { + return { + success, + invalidDomains + }; + }); + }; + }, + + removeDomainBlock: function removeDomainBlock(domain) { + return function (dispatch, getState) { + return Promise.try(() => { + const id = getState().admin.blockedInstances[domain].id; + return dispatch(apiCall("DELETE", `/api/v1/admin/domain_blocks/${id}`)); + }).then((removed) => { + return dispatch(admin.removeDomainBlock(removed.domain)); + }); + }; + }, + + mediaCleanup: function mediaCleanup(days) { + return function (dispatch, _getState) { + return Promise.try(() => { + return dispatch(apiCall("POST", `/api/v1/admin/media_cleanup?remote_cache_days=${days}`)); + }); + }; + }, + + fetchCustomEmoji: function fetchCustomEmoji() { + return function (dispatch, _getState) { + return Promise.try(() => { + return dispatch(apiCall("GET", "/api/v1/custom_emojis")); + }).then((emoji) => { + return dispatch(admin.setEmoji(emoji)); + }); + }; + }, + + newEmoji: function newEmoji() { + return function (dispatch, getState) { + return Promise.try(() => { + const state = getState().admin.newEmoji; + + const update = getChanges(state, { + formKeys: ["shortcode"], + fileKeys: ["image"] + }); + + return dispatch(apiCall("POST", "/api/v1/admin/custom_emojis", update, "form")); + }).then((emoji) => { + return dispatch(admin.addEmoji(emoji)); + }); + }; + } + }; + return adminAPI; +};
\ 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..e699011bd --- /dev/null +++ b/web/source/settings-panel/lib/api/index.js @@ -0,0 +1,185 @@ +/* + 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 <http://www.gnu.org/licenses/>. +*/ + +"use strict"; + +const Promise = require("bluebird"); +const { isPlainObject } = require("is-plain-object"); +const d = require("dotty"); + +const { APIError, AuthenticationError } = require("../errors"); +const { setInstanceInfo, setNamedInstanceInfo } = require("../../redux/reducers/instances").actions; +const oauth = require("../../redux/reducers/oauth").actions; + +function apiCall(method, route, payload, type = "json") { + 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); + let [path, query] = route.split("?"); + url.pathname = path; + if (query != undefined) { + url.search = query; + } + let body; + + let headers = { + "Accept": "application/json", + }; + + if (payload != undefined) { + if (type == "json") { + headers["Content-Type"] = "application/json"; + body = JSON.stringify(payload); + } else if (type == "form") { + const formData = new FormData(); + Object.entries(payload).forEach(([key, val]) => { + if (isPlainObject(val)) { + Object.entries(val).forEach(([key2, val2]) => { + if (val2 != undefined) { + formData.set(`${key}[${key2}]`, val2); + } + }); + } else { + if (val != undefined) { + formData.set(key, val); + } + } + }); + body = formData; + } + } + + 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 || res.status == 403)) { + // stored access token is invalid + throw new AuthenticationError("401: Authentication error", {json, status: res.status}); + } else { + throw new APIError(json.error, { json }); + } + } else { + return json; + } + }); + }; +} + +function getChanges(state, keys) { + const { formKeys = [], fileKeys = [], renamedKeys = {} } = keys; + const update = {}; + + formKeys.forEach((key) => { + let value = d.get(state, key); + if (value == undefined) { + return; + } + if (renamedKeys[key]) { + key = renamedKeys[key]; + } + d.put(update, key, value); + }); + + fileKeys.forEach((key) => { + let file = d.get(state, `${key}File`); + if (file != undefined) { + if (renamedKeys[key]) { + key = renamedKeys[key]; + } + d.put(update, key, file); + } + }); + + return update; +} + +function getCurrentUrl() { + return `${window.location.origin}${window.location.pathname}`; +} + +function fetchInstanceWithoutStore(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("GET", "/api/v1/instance")(dispatch, () => fakeState); + }).then((json) => { + if (json && json.uri) { // TODO: validate instance json more? + dispatch(setNamedInstanceInfo([domain, json])); + return json; + } + }); + }; +} + +function fetchInstance() { + return function (dispatch, _getState) { + return Promise.try(() => { + return dispatch(apiCall("GET", "/api/v1/instance")); + }).then((json) => { + if (json && json.uri) { + dispatch(setInstanceInfo(json)); + return json; + } + }); + }; +} + +let submoduleArgs = { apiCall, getCurrentUrl, getChanges }; + +module.exports = { + instance: { + fetchWithoutStore: fetchInstanceWithoutStore, + fetch: fetchInstance + }, + oauth: require("./oauth")(submoduleArgs), + user: require("./user")(submoduleArgs), + admin: require("./admin")(submoduleArgs), + apiCall, + getChanges +};
\ 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..76d0e9d2f --- /dev/null +++ b/web/source/settings-panel/lib/api/oauth.js @@ -0,0 +1,124 @@ +/* + 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 <http://www.gnu.org/licenses/>. +*/ + +"use strict"; + +const Promise = require("bluebird"); + +const { OAUTHError, AuthenticationError } = require("../errors"); + +const oauth = require("../../redux/reducers/oauth").actions; +const temporary = require("../../redux/reducers/temporary").actions; +const admin = require("../../redux/reducers/admin").actions; + +module.exports = function oauthAPI({ apiCall, getCurrentUrl }) { + return { + + register: function register(scopes = []) { + return function (dispatch, _getState) { + return Promise.try(() => { + 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)); + }); + }; + }, + + 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 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) => { + window.history.replaceState({}, document.title, window.location.pathname); + return dispatch(oauth.login(json)); + }); + }; + }, + + checkIfAdmin: function checkIfAdmin() { + return function (dispatch, getState) { + const state = getState(); + let stored = state.oauth.isAdmin; + if (stored != undefined) { + return stored; + } + + // newer GoToSocial version will include a `role` in the Account data, check that first + // TODO: check account data for admin status + + // no role info, try fetching an admin-only route and see if we get an error + return Promise.try(() => { + return dispatch(apiCall("GET", "/api/v1/admin/domain_blocks")); + }).then((data) => { + return Promise.all([ + dispatch(oauth.setAdmin(true)), + dispatch(admin.setBlockedInstances(data)) + ]); + }).catch(AuthenticationError, () => { + return dispatch(oauth.setAdmin(false)); + }); + }; + }, + + 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 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..18b54bd73 --- /dev/null +++ b/web/source/settings-panel/lib/api/user.js @@ -0,0 +1,67 @@ +/* + 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 <http://www.gnu.org/licenses/>. +*/ + +"use strict"; + +const Promise = require("bluebird"); + +const user = require("../../redux/reducers/user").actions; + +module.exports = function ({ apiCall, getChanges }) { + function updateCredentials(selector, keys) { + return function (dispatch, getState) { + return Promise.try(() => { + const state = selector(getState()); + + const update = getChanges(state, keys); + + return dispatch(apiCall("PATCH", "/api/v1/accounts/update_credentials", update, "form")); + }).then((account) => { + return dispatch(user.setAccount(account)); + }); + }; + } + + 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)); + }); + }; + }, + + updateProfile: function updateProfile() { + const formKeys = ["display_name", "locked", "source", "custom_css", "source.note"]; + const renamedKeys = { + "source.note": "note" + }; + const fileKeys = ["header", "avatar"]; + + return updateCredentials((state) => state.user.profile, {formKeys, renamedKeys, fileKeys}); + }, + + updateSettings: function updateProfile() { + const formKeys = ["source"]; + + return updateCredentials((state) => state.user.settings, {formKeys}); + } + }; +};
\ No newline at end of file diff --git a/web/source/settings-panel/lib/errors.js b/web/source/settings-panel/lib/errors.js new file mode 100644 index 000000000..c2f781cb2 --- /dev/null +++ b/web/source/settings-panel/lib/errors.js @@ -0,0 +1,27 @@ +/* + 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 <http://www.gnu.org/licenses/>. +*/ + +"use strict"; + +const createError = require("create-error"); + +module.exports = { + APIError: createError("APIError"), + OAUTHError: createError("OAUTHError"), + AuthenticationError: createError("AuthenticationError"), +};
\ No newline at end of file diff --git a/web/source/settings-panel/lib/get-views.js b/web/source/settings-panel/lib/get-views.js new file mode 100644 index 000000000..39f627435 --- /dev/null +++ b/web/source/settings-panel/lib/get-views.js @@ -0,0 +1,102 @@ +/* + 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 <http://www.gnu.org/licenses/>. +*/ + +"use strict"; + +const React = require("react"); +const Redux = require("react-redux"); +const { Link, Route, Switch, Redirect } = require("wouter"); +const { ErrorBoundary } = require("react-error-boundary"); + +const ErrorFallback = require("../components/error"); +const NavButton = require("../components/nav-button"); + +function urlSafe(str) { + return str.toLowerCase().replace(/\s+/g, "-"); +} + +module.exports = function getViews(struct) { + const sidebar = { + all: [], + admin: [], + }; + + const panelRouter = { + all: [], + admin: [], + }; + + Object.entries(struct).forEach(([name, entries]) => { + let sidebarEl = sidebar.all; + let panelRouterEl = panelRouter.all; + + if (entries.adminOnly) { + sidebarEl = sidebar.admin; + panelRouterEl = panelRouter.admin; + delete entries.adminOnly; + } + + let base = `/settings/${urlSafe(name)}`; + + let links = []; + + let firstRoute; + + Object.entries(entries).forEach(([name, ViewComponent]) => { + let url = `${base}/${urlSafe(name)}`; + + if (firstRoute == undefined) { + firstRoute = url; + } + + panelRouterEl.push(( + <Route path={`${url}/:page?`} key={url}> + <ErrorBoundary FallbackComponent={ErrorFallback} onReset={() => { }}> + {/* FIXME: implement onReset */} + <ViewComponent /> + </ErrorBoundary> + </Route> + )); + + links.push( + <NavButton key={url} href={url} name={name} /> + ); + }); + + panelRouterEl.push( + <Route key={base} path={base}> + <Redirect to={firstRoute} /> + </Route> + ); + + sidebarEl.push( + <React.Fragment key={name}> + <Link href={firstRoute}> + <a> + <h2>{name}</h2> + </a> + </Link> + <nav> + {links} + </nav> + </React.Fragment> + ); + }); + + return { sidebar, panelRouter }; +};
\ No newline at end of file diff --git a/web/source/settings-panel/lib/panel.js b/web/source/settings-panel/lib/panel.js new file mode 100644 index 000000000..df723bc74 --- /dev/null +++ b/web/source/settings-panel/lib/panel.js @@ -0,0 +1,134 @@ +/* + 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 <http://www.gnu.org/licenses/>. +*/ + +"use strict"; + +const Promise = require("bluebird"); +const React = require("react"); +const ReactDom = require("react-dom"); + +const oauthLib = require("./oauth"); + +module.exports = function createPanel(clientName, scope, Component) { + ReactDom.render(<Panel/>, document.getElementById("root")); + + function Panel() { + const [oauth, setOauth] = React.useState(); + const [hasAuth, setAuth] = React.useState(false); + const [oauthState, setOauthState] = React.useState(localStorage.getItem("oauth")); + + React.useEffect(() => { + let state = localStorage.getItem("oauth"); + if (state != undefined) { + state = JSON.parse(state); + let restoredOauth = oauthLib(state.config, state); + Promise.try(() => { + return restoredOauth.callback(); + }).then(() => { + setAuth(true); + }); + setOauth(restoredOauth); + } + }, [setAuth, setOauth]); + + if (!hasAuth && oauth && oauth.isAuthorized()) { + setAuth(true); + } + + if (oauth && oauth.isAuthorized()) { + return <Component oauth={oauth} />; + } else if (oauthState != undefined) { + return "processing oauth..."; + } else { + return <Auth setOauth={setOauth} />; + } + } + + function Auth({setOauth}) { + const [ instance, setInstance ] = React.useState(""); + + React.useEffect(() => { + let isStillMounted = true; + // check if current domain runs an instance + let thisUrl = new URL(window.location.origin); + thisUrl.pathname = "/api/v1/instance"; + Promise.try(() => { + return fetch(thisUrl.href); + }).then((res) => { + if (res.status == 200) { + return res.json(); + } + }).then((json) => { + if (json && json.uri && isStillMounted) { + setInstance(json.uri); + } + }).catch((e) => { + console.log("error checking instance response:", e); + }); + + return () => { + // cleanup function + isStillMounted = false; + }; + }, []); + + function doAuth() { + return Promise.try(() => { + return new URL(instance); + }).catch(TypeError, () => { + return new URL(`https://${instance}`); + }).then((parsedURL) => { + let url = parsedURL.toString(); + let oauth = oauthLib({ + instance: url, + client_name: clientName, + scope: scope, + website: window.location.href + }); + setOauth(oauth); + setInstance(url); + return oauth.register().then(() => { + return oauth; + }); + }).then((oauth) => { + return oauth.authorize(); + }).catch((e) => { + console.log("error authenticating:", e); + }); + } + + function updateInstance(e) { + if (e.key == "Enter") { + doAuth(); + } else { + setInstance(e.target.value); + } + } + + return ( + <section className="login"> + <h1>OAUTH Login:</h1> + <form onSubmit={(e) => e.preventDefault()}> + <label htmlFor="instance">Instance: </label> + <input value={instance} onChange={updateInstance} id="instance"/> + <button onClick={doAuth}>Authenticate</button> + </form> + </section> + ); + } +};
\ No newline at end of file diff --git a/web/source/settings-panel/lib/submit.js b/web/source/settings-panel/lib/submit.js new file mode 100644 index 000000000..f268b5cf9 --- /dev/null +++ b/web/source/settings-panel/lib/submit.js @@ -0,0 +1,48 @@ +/* + 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 <http://www.gnu.org/licenses/>. +*/ + +"use strict"; + +const Promise = require("bluebird"); + +module.exports = function submit(func, { + setStatus, setError, + startStatus="PATCHing", successStatus="Saved!", + onSuccess, + onError +}) { + return function() { + setStatus(startStatus); + setError(""); + return Promise.try(() => { + return func(); + }).then(() => { + setStatus(successStatus); + if (onSuccess != undefined) { + return onSuccess(); + } + }).catch((e) => { + setError(e.message); + setStatus(""); + console.error(e); + if (onError != undefined) { + onError(e); + } + }); + }; +};
\ No newline at end of file |