/* 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"); function getCurrentUrl() { return window.location.origin + window.location.pathname; // strips ?query=string and #hash } module.exports = function oauthClient(config, initState) { /* config: instance: instance domain (https://testingtesting123.xyz) client_name: "GoToSocial Admin Panel" scope: [] website: */ let state = initState; if (initState == undefined) { state = localStorage.getItem("oauth"); if (state == undefined) { state = { config }; storeState(); } else { state = JSON.parse(state); } } function storeState() { localStorage.setItem("oauth", JSON.stringify(state)); } /* register app /api/v1/apps */ function register() { if (state.client_id != undefined) { return true; // we already have a registration } let url = new URL(config.instance); url.pathname = "/api/v1/apps"; return fetch(url.href, { method: "POST", headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ client_name: config.client_name, redirect_uris: getCurrentUrl(), scopes: config.scope.join(" "), website: getCurrentUrl() }) }).then((res) => { if (res.status != 200) { throw res; } return res.json(); }).then((json) => { state.client_id = json.client_id; state.client_secret = json.client_secret; storeState(); }); } /* authorize: /oauth/authorize ?client_id=CLIENT_ID &redirect_uri=window.location.href &response_type=code &scope=admin */ function authorize() { let url = new URL(config.instance); url.pathname = "/oauth/authorize"; url.searchParams.set("client_id", state.client_id); url.searchParams.set("redirect_uri", getCurrentUrl()); url.searchParams.set("response_type", "code"); url.searchParams.set("scope", config.scope.join(" ")); window.location.assign(url.href); } function callback() { if (state.access_token != undefined) { return; // we're already done :) } let params = (new URL(window.location)).searchParams; let token = params.get("code"); if (token != null) { console.log("got token callback:", token); } return authorizeToken(token) .catch((e) => { console.log("Error processing oauth callback:", e); logout(); // just to be sure }); } function authorizeToken(token) { let url = new URL(config.instance); url.pathname = "/oauth/token"; return fetch(url.href, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ client_id: state.client_id, client_secret: state.client_secret, redirect_uri: getCurrentUrl(), grant_type: "authorization_code", code: token }) }).then((res) => { if (res.status != 200) { throw res; } return res.json(); }).then((json) => { state.access_token = json.access_token; storeState(); window.location = getCurrentUrl(); // clear ?token= }); } function isAuthorized() { return (state.access_token != undefined); } function apiRequest(path, method, data, type="json", accept="json") { if (!isAuthorized()) { throw new Error("Not Authenticated"); } let url = new URL(config.instance); let [p, s] = path.split("?"); url.pathname = p; if (s != undefined) { url.search = s; } let headers = { "Authorization": `Bearer ${state.access_token}`, "Accept": accept == "json" ? "application/json" : "*/*" }; let body = data; if (type == "json" && body != undefined) { headers["Content-Type"] = "application/json"; body = JSON.stringify(data); } return fetch(url.href, { method, headers, body }).then((res) => { return Promise.all([res.json(), res]); }).then(([json, res]) => { if (res.status != 200) { if (json.error) { throw new Error(json.error); } else { throw new Error(`${res.status}: ${res.statusText}`); } } else { return json; } }).catch(e => { if (e instanceof SyntaxError) { throw new Error("Error: The GtS API returned a non-json error. This usually means a network problem, or an issue with your instance's reverse proxy configuration.", {cause: e}); } else { throw e; } }); } function logout() { let url = new URL(config.instance); url.pathname = "/oauth/revoke"; return fetch(url.href, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ client_id: state.client_id, client_secret: state.client_secret, token: state.access_token, }) }).then((res) => { if (res.status != 200) { // GoToSocial doesn't actually implement this route yet, // so error is to be expected return; } return res.json(); }).catch(() => { // see above }).then(() => { localStorage.removeItem("oauth"); window.location = getCurrentUrl(); }); } return { register, authorize, callback, isAuthorized, apiRequest, logout }; };