oauth.js (4579B)
1 /* 2 GoToSocial 3 Copyright (C) GoToSocial Authors admin@gotosocial.org 4 SPDX-License-Identifier: AGPL-3.0-or-later 5 6 This program is free software: you can redistribute it and/or modify 7 it under the terms of the GNU Affero General Public License as published by 8 the Free Software Foundation, either version 3 of the License, or 9 (at your option) any later version. 10 11 This program is distributed in the hope that it will be useful, 12 but WITHOUT ANY WARRANTY; without even the implied warranty of 13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 GNU Affero General Public License for more details. 15 16 You should have received a copy of the GNU Affero General Public License 17 along with this program. If not, see <http://www.gnu.org/licenses/>. 18 */ 19 20 "use strict"; 21 22 const Promise = require("bluebird"); 23 24 const base = require("./base"); 25 const { unwrapRes } = require("./lib"); 26 const oauth = require("../../redux/oauth").actions; 27 28 function getSettingsURL() { 29 /* needed in case the settings interface isn't hosted at /settings but 30 some subpath like /gotosocial/settings. Other parts of the code don't 31 take this into account yet so mostly future-proofing. 32 33 Also drops anything past /settings/, because authorization urls that are too long 34 get rejected by GTS. 35 */ 36 let [pre, _past] = window.location.pathname.split("/settings"); 37 return `${window.location.origin}${pre}/settings`; 38 } 39 40 const SETTINGS_URL = getSettingsURL(); 41 42 const endpoints = (build) => ({ 43 verifyCredentials: build.query({ 44 providesTags: (_res, error) => 45 error == undefined 46 ? ["Auth"] 47 : [], 48 queryFn: (_arg, api, _extraOpts, baseQuery) => { 49 const state = api.getState(); 50 51 return Promise.try(() => { 52 // Process callback code first, if available 53 if (state.oauth.loginState == "callback") { 54 let urlParams = new URLSearchParams(window.location.search); 55 let code = urlParams.get("code"); 56 57 if (code == undefined) { 58 throw { 59 message: "Waiting for callback, but no ?code= provided in url." 60 }; 61 } else { 62 let app = state.oauth.registration; 63 64 if (app == undefined || app.client_id == undefined) { 65 throw { 66 message: "No stored registration data, can't finish login flow." 67 }; 68 } 69 70 return baseQuery({ 71 method: "POST", 72 url: "/oauth/token", 73 body: { 74 client_id: app.client_id, 75 client_secret: app.client_secret, 76 redirect_uri: SETTINGS_URL, 77 grant_type: "authorization_code", 78 code: code 79 } 80 }).then(unwrapRes).then((token) => { 81 // remove ?code= from url 82 window.history.replaceState({}, document.title, window.location.pathname); 83 api.dispatch(oauth.setToken(token)); 84 }); 85 } 86 } 87 }).then(() => { 88 return baseQuery({ 89 url: `/api/v1/accounts/verify_credentials` 90 }); 91 }).catch((e) => { 92 return { error: e }; 93 }); 94 } 95 }), 96 authorizeFlow: build.mutation({ 97 queryFn: (formData, api, _extraOpts, baseQuery) => { 98 let instance; 99 const state = api.getState(); 100 101 return Promise.try(() => { 102 if (!formData.instance.startsWith("http")) { 103 formData.instance = `https://${formData.instance}`; 104 } 105 instance = new URL(formData.instance).origin; 106 107 const stored = state.oauth.instance; 108 if (stored?.instance == instance && stored.registration) { 109 return stored.registration; 110 } 111 112 return baseQuery({ 113 method: "POST", 114 baseUrl: instance, 115 url: "/api/v1/apps", 116 body: { 117 client_name: "GoToSocial Settings", 118 scopes: formData.scopes, 119 redirect_uris: SETTINGS_URL, 120 website: SETTINGS_URL 121 } 122 }).then(unwrapRes).then((app) => { 123 app.scopes = formData.scopes; 124 125 api.dispatch(oauth.authorize({ 126 instance: instance, 127 registration: app, 128 loginState: "callback", 129 expectingRedirect: true 130 })); 131 132 return app; 133 }); 134 }).then((app) => { 135 let url = new URL(instance); 136 url.pathname = "/oauth/authorize"; 137 url.searchParams.set("client_id", app.client_id); 138 url.searchParams.set("redirect_uri", SETTINGS_URL); 139 url.searchParams.set("response_type", "code"); 140 url.searchParams.set("scope", app.scopes); 141 142 let redirectURL = url.toString(); 143 window.location.assign(redirectURL); 144 145 return { data: null }; 146 }).catch((e) => { 147 return { error: e }; 148 }); 149 }, 150 }), 151 logout: build.mutation({ 152 queryFn: (_arg, api) => { 153 api.dispatch(oauth.remove()); 154 return { data: null }; 155 }, 156 invalidatesTags: ["Auth"] 157 }) 158 }); 159 160 module.exports = base.injectEndpoints({ endpoints });