summaryrefslogtreecommitdiff
path: root/web/source/settings/lib/api
diff options
context:
space:
mode:
Diffstat (limited to 'web/source/settings/lib/api')
-rw-r--r--web/source/settings/lib/api/admin.js192
-rw-r--r--web/source/settings/lib/api/index.js185
-rw-r--r--web/source/settings/lib/api/oauth.js124
-rw-r--r--web/source/settings/lib/api/user.js67
4 files changed, 568 insertions, 0 deletions
diff --git a/web/source/settings/lib/api/admin.js b/web/source/settings/lib/api/admin.js
new file mode 100644
index 000000000..1df47b693
--- /dev/null
+++ b/web/source/settings/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/lib/api/index.js b/web/source/settings/lib/api/index.js
new file mode 100644
index 000000000..e699011bd
--- /dev/null
+++ b/web/source/settings/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/lib/api/oauth.js b/web/source/settings/lib/api/oauth.js
new file mode 100644
index 000000000..76d0e9d2f
--- /dev/null
+++ b/web/source/settings/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/lib/api/user.js b/web/source/settings/lib/api/user.js
new file mode 100644
index 000000000..18b54bd73
--- /dev/null
+++ b/web/source/settings/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