summaryrefslogtreecommitdiff
path: root/web/source/settings-panel/lib
diff options
context:
space:
mode:
Diffstat (limited to 'web/source/settings-panel/lib')
-rw-r--r--web/source/settings-panel/lib/api/admin.js192
-rw-r--r--web/source/settings-panel/lib/api/index.js185
-rw-r--r--web/source/settings-panel/lib/api/oauth.js124
-rw-r--r--web/source/settings-panel/lib/api/user.js67
-rw-r--r--web/source/settings-panel/lib/errors.js27
-rw-r--r--web/source/settings-panel/lib/get-views.js102
-rw-r--r--web/source/settings-panel/lib/panel.js134
-rw-r--r--web/source/settings-panel/lib/submit.js48
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