mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-11-01 06:50:00 +00:00
full redux oauth implementation, with basic error handling
This commit is contained in:
parent
a4bb869d0f
commit
5f80099eee
13 changed files with 312 additions and 397 deletions
|
@ -46,6 +46,7 @@ $blue3: #89caff; /* hover/selected accent to $blue2, can be used with $gray1 (7.
|
|||
|
||||
$error1: #860000; /* Error border/foreground text, can be used with $error2 (5.0), $white1 (10), $white2 (5.1) */
|
||||
$error2: #ff9796; /* Error background text, can be used with $error1 (5.0), $gray1 (6.6), $gray2 (5.3), $gray3 (4.8) */
|
||||
$error-link: #185F8C; /* Error link text, can be used with $error2 (5.54) */
|
||||
|
||||
$fg: $white1;
|
||||
$bg: $gray1;
|
||||
|
@ -93,3 +94,6 @@ $settings-nav-fg-hover: $gray1;
|
|||
|
||||
$settings-nav-bg-active: $orange1;
|
||||
$settings-nav-fg-active: $gray1;
|
||||
|
||||
$error-fg: $error1;
|
||||
$error-bg: $error2;
|
|
@ -16,6 +16,7 @@
|
|||
"bluebird": "^3.7.2",
|
||||
"browserify": "^17.0.0",
|
||||
"browserlist": "^1.0.1",
|
||||
"create-error": "^0.3.1",
|
||||
"css-extract": "^2.0.0",
|
||||
"eslint-plugin-react": "^7.24.0",
|
||||
"express": "^4.18.1",
|
||||
|
|
|
@ -1,97 +0,0 @@
|
|||
/*
|
||||
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 oauthLib = require("../lib/oauth");
|
||||
|
||||
module.exports = 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: "GotoSocial",
|
||||
scope: ["admin"],
|
||||
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>
|
||||
);
|
||||
};
|
|
@ -22,10 +22,10 @@ const Promise = require("bluebird");
|
|||
const React = require("react");
|
||||
const Redux = require("react-redux");
|
||||
|
||||
const { setInstance } = require("../redux/reducers/instances").actions;
|
||||
const { updateInstance, updateRegistration } = require("../lib/api");
|
||||
const { setInstance } = require("../redux/reducers/oauth").actions;
|
||||
const api = require("../lib/api");
|
||||
|
||||
module.exports = function Login() {
|
||||
module.exports = function Login({error}) {
|
||||
const dispatch = Redux.useDispatch();
|
||||
const [ instanceField, setInstanceField ] = React.useState("");
|
||||
const [ errorMsg, setErrorMsg ] = React.useState();
|
||||
|
@ -35,7 +35,7 @@ module.exports = function Login() {
|
|||
// check if current domain runs an instance
|
||||
Promise.try(() => {
|
||||
console.log("trying", window.location.origin);
|
||||
return dispatch(updateInstance(window.location.origin));
|
||||
return dispatch(api.instance.fetch(window.location.origin));
|
||||
}).then((json) => {
|
||||
if (instanceFieldRef.current.length == 0) { // user hasn't started typing yet
|
||||
dispatch(setInstance(json.uri));
|
||||
|
@ -49,13 +49,20 @@ module.exports = function Login() {
|
|||
|
||||
function tryInstance() {
|
||||
Promise.try(() => {
|
||||
return dispatch(updateInstance(instanceFieldRef.current)).catch((e) => {
|
||||
return dispatch(api.instance.fetch(instanceFieldRef.current)).catch((e) => {
|
||||
// TODO: clearer error messages for common errors
|
||||
console.log(e);
|
||||
throw e;
|
||||
});
|
||||
}).then((instance) => {
|
||||
// return dispatch(updateRegistration);
|
||||
dispatch(setInstance(instance.uri));
|
||||
|
||||
return dispatch(api.oauth.register()).catch((e) => {
|
||||
console.log(e);
|
||||
throw e;
|
||||
});
|
||||
}).then(() => {
|
||||
return dispatch(api.oauth.authorize()); // will send user off-page
|
||||
}).catch((e) => {
|
||||
setErrorMsg(
|
||||
<>
|
||||
|
@ -78,6 +85,7 @@ module.exports = function Login() {
|
|||
return (
|
||||
<section className="login">
|
||||
<h1>OAUTH Login:</h1>
|
||||
{error}
|
||||
<form onSubmit={(e) => e.preventDefault()}>
|
||||
<label htmlFor="instance">Instance: </label>
|
||||
<input value={instanceField} onChange={updateInstanceField} id="instance"/>
|
||||
|
|
|
@ -27,11 +27,9 @@ const { Provider } = require("react-redux");
|
|||
const { PersistGate } = require("redux-persist/integration/react");
|
||||
|
||||
const { store, persistor } = require("./redux");
|
||||
const api = require("./lib/api");
|
||||
|
||||
const Login = require("./components/login");
|
||||
const ErrorFallback = require("./components/error");
|
||||
|
||||
const oauthLib = require("./lib/oauth");
|
||||
|
||||
require("./style.css");
|
||||
|
||||
|
@ -59,49 +57,99 @@ const nav = {
|
|||
const { sidebar, panelRouter } = require("./lib/generate-views")(nav);
|
||||
|
||||
function App() {
|
||||
const { loggedIn } = Redux.useSelector((state) => state.oauth);
|
||||
const dispatch = Redux.useDispatch();
|
||||
const { loginState } = Redux.useSelector((state) => state.oauth);
|
||||
const reduxTempStatus = Redux.useSelector((state) => state.temporary.status);
|
||||
const [ errorMsg, setErrorMsg ] = React.useState();
|
||||
const [ tokenChecked, setTokenChecked ] = React.useState(false);
|
||||
|
||||
// const [oauth, setOauth] = React.useState();
|
||||
// const [hasAuth, setAuth] = React.useState(false);
|
||||
// const [oauthState, _setOauthState] = React.useState(localStorage.getItem("oauth"));
|
||||
React.useEffect(() => {
|
||||
Promise.try(() => {
|
||||
// Process OAUTH authorization token from URL if available
|
||||
if (loginState == "callback") {
|
||||
let urlParams = new URLSearchParams(window.location.search);
|
||||
let code = urlParams.get("code");
|
||||
|
||||
// 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 (code == undefined) {
|
||||
setErrorMsg(new Error("Waiting for OAUTH callback but no ?code= provided. You can try logging in again:"));
|
||||
} else {
|
||||
return dispatch(api.oauth.fetchToken(code));
|
||||
}
|
||||
}
|
||||
}).then(() => {
|
||||
// Check currently stored auth token for validity if available
|
||||
if (loginState == "callback" || loginState == "login") {
|
||||
return dispatch(api.oauth.verify());
|
||||
}
|
||||
}).then(() => {
|
||||
setTokenChecked(true);
|
||||
}).catch((e) => {
|
||||
setErrorMsg(e);
|
||||
console.error(e.message);
|
||||
});
|
||||
}, []);
|
||||
|
||||
// if (!hasAuth && oauth && oauth.isAuthorized()) {
|
||||
// setAuth(true);
|
||||
// }
|
||||
let ErrorElement = null;
|
||||
if (errorMsg != undefined) {
|
||||
ErrorElement = (
|
||||
<div className="error">
|
||||
<b>{errorMsg.type}</b>
|
||||
<span>{errorMsg.message}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (loggedIn) {
|
||||
const LogoutElement = (
|
||||
<button className="logout" onClick={() => {dispatch(api.oauth.logout());}}>
|
||||
Log out
|
||||
</button>
|
||||
);
|
||||
|
||||
if (reduxTempStatus != undefined) {
|
||||
return (
|
||||
<section>
|
||||
{reduxTempStatus}
|
||||
</section>
|
||||
);
|
||||
} else if (tokenChecked && loginState == "login") {
|
||||
return (
|
||||
<>
|
||||
<div className="sidebar">
|
||||
{sidebar}
|
||||
{/* <button className="logout" onClick={oauth.logout}>Log out</button> */}
|
||||
{LogoutElement}
|
||||
</div>
|
||||
<section className="with-sidebar">
|
||||
{ErrorElement}
|
||||
<Switch>
|
||||
{panelRouter}
|
||||
</Switch>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
} else if (loginState == "none") {
|
||||
return (
|
||||
<Login />
|
||||
<Login error={ErrorElement}/>
|
||||
);
|
||||
} else {
|
||||
let status;
|
||||
|
||||
if (loginState == "login") {
|
||||
status = "Verifying stored login...";
|
||||
} else if (loginState == "callback") {
|
||||
status = "Processing OAUTH callback...";
|
||||
}
|
||||
|
||||
return (
|
||||
<section>
|
||||
<div>
|
||||
{status}
|
||||
</div>
|
||||
{ErrorElement}
|
||||
{LogoutElement}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function Main() {
|
||||
|
|
|
@ -19,10 +19,17 @@
|
|||
"use strict";
|
||||
|
||||
const Promise = require("bluebird");
|
||||
const { setRegistration } = require("../redux/reducers/oauth").actions;
|
||||
|
||||
const { APIError, OAUTHError } = require("./errors");
|
||||
const oauth = require("../redux/reducers/oauth").actions;
|
||||
const temporary = require("../redux/reducers/temporary").actions;
|
||||
const { setInstanceInfo } = require("../redux/reducers/instances").actions;
|
||||
|
||||
function apiCall(base, method, route, {payload, headers={}}) {
|
||||
function apiCall(state, method, route, payload) {
|
||||
let base = state.oauth.instance;
|
||||
let auth = state.oauth.token;
|
||||
console.log(method, base, route, auth);
|
||||
|
||||
return Promise.try(() => {
|
||||
let url = new URL(base);
|
||||
url.pathname = route;
|
||||
|
@ -32,21 +39,34 @@ function apiCall(base, method, route, {payload, headers={}}) {
|
|||
body = JSON.stringify(payload);
|
||||
}
|
||||
|
||||
let fetchHeaders = {
|
||||
"Content-Type": "application/json",
|
||||
...headers
|
||||
let headers = {
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json"
|
||||
};
|
||||
|
||||
if (auth != undefined) {
|
||||
headers["Authorization"] = auth;
|
||||
}
|
||||
|
||||
return fetch(url.toString(), {
|
||||
method: method,
|
||||
headers: fetchHeaders,
|
||||
body: body
|
||||
method,
|
||||
headers,
|
||||
body
|
||||
});
|
||||
}).then((res) => {
|
||||
if (res.status == 200) {
|
||||
return res.json();
|
||||
let ok = res.ok;
|
||||
|
||||
// try parse json even with error
|
||||
let json = res.json().catch((e) => {
|
||||
throw new APIError(`JSON parsing error: ${e.message}`);
|
||||
});
|
||||
|
||||
return Promise.all([ok, json]);
|
||||
}).then(([ok, json]) => {
|
||||
if (!ok) {
|
||||
throw new APIError(json.error, {json});
|
||||
} else {
|
||||
throw res;
|
||||
return json;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -55,40 +75,122 @@ function getCurrentUrl() {
|
|||
return `${window.location.origin}${window.location.pathname}`;
|
||||
}
|
||||
|
||||
function updateInstance(domain) {
|
||||
function fetchInstance(domain) {
|
||||
return function(dispatch, getState) {
|
||||
/* check if domain is valid instance, then register client if needed */
|
||||
|
||||
return Promise.try(() => {
|
||||
return apiCall(domain, "GET", "/api/v1/instance", {
|
||||
headers: {
|
||||
"Content-Type": "text/plain"
|
||||
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(fakeState, "GET", "/api/v1/instance");
|
||||
}).then((json) => {
|
||||
if (json && json.uri) { // TODO: validate instance json more?
|
||||
dispatch(setInstanceInfo(json.uri, json));
|
||||
dispatch(setInstanceInfo([json.uri, json]));
|
||||
return json;
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function updateRegistration() {
|
||||
function fetchRegistration(scopes=[]) {
|
||||
return function(dispatch, getState) {
|
||||
let base = getState().oauth.instance;
|
||||
return Promise.try(() => {
|
||||
return apiCall(base, "POST", "/api/v1/apps", {
|
||||
return apiCall(getState(), "POST", "/api/v1/apps", {
|
||||
client_name: "GoToSocial Settings",
|
||||
scopes: "write admin",
|
||||
scopes: scopes.join(" "),
|
||||
redirect_uris: getCurrentUrl(),
|
||||
website: getCurrentUrl()
|
||||
});
|
||||
}).then((json) => {
|
||||
console.log(json);
|
||||
dispatch(setRegistration(base, json));
|
||||
json.scopes = scopes;
|
||||
dispatch(oauth.setRegistration(json));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { updateInstance, updateRegistration };
|
||||
function startAuthorize() {
|
||||
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);
|
||||
};
|
||||
}
|
||||
|
||||
function fetchToken(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 apiCall(getState(), "POST", "/oauth/token", {
|
||||
client_id: reg.client_id,
|
||||
client_secret: reg.client_secret,
|
||||
redirect_uri: getCurrentUrl(),
|
||||
grant_type: "authorization_code",
|
||||
code: code
|
||||
});
|
||||
}).then((json) => {
|
||||
console.log(json);
|
||||
window.history.replaceState({}, document.title, window.location.pathname);
|
||||
return dispatch(oauth.login(json));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function verifyAuth() {
|
||||
return function(dispatch, getState) {
|
||||
console.log(getState());
|
||||
return Promise.try(() => {
|
||||
return apiCall(getState(), "GET", "/api/v1/accounts/verify_credentials");
|
||||
}).then((account) => {
|
||||
console.log(account);
|
||||
}).catch((e) => {
|
||||
dispatch(oauth.remove());
|
||||
throw e;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function oauthLogout() {
|
||||
return function(dispatch, _getState) {
|
||||
// TODO: GoToSocial does not have a logout API route yet
|
||||
|
||||
return dispatch(oauth.remove());
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
instance: {
|
||||
fetch: fetchInstance
|
||||
},
|
||||
oauth: {
|
||||
register: fetchRegistration,
|
||||
authorize: startAuthorize,
|
||||
fetchToken,
|
||||
verify: verifyAuth,
|
||||
logout: oauthLogout
|
||||
}
|
||||
};
|
26
web/source/settings-panel/lib/errors.js
Normal file
26
web/source/settings-panel/lib/errors.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
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"),
|
||||
};
|
|
@ -1,227 +0,0 @@
|
|||
/*
|
||||
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");
|
||||
|
||||
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
|
||||
};
|
||||
};
|
|
@ -27,12 +27,14 @@ const persistConfig = {
|
|||
key: "gotosocial-settings",
|
||||
storage: require("redux-persist/lib/storage").default,
|
||||
stateReconciler: require("redux-persist/lib/stateReconciler/autoMergeLevel2").default,
|
||||
whitelist: ['oauth']
|
||||
whitelist: ["oauth"],
|
||||
blacklist: ["temporary"]
|
||||
};
|
||||
|
||||
const combinedReducers = combineReducers({
|
||||
oauth: require("./reducers/oauth").reducer,
|
||||
instances: require("./reducers/instances").reducer,
|
||||
temporary: require("./reducers/temporary").reducer,
|
||||
});
|
||||
|
||||
const persistedReducer = persistReducer(persistConfig, combinedReducers);
|
||||
|
|
|
@ -24,12 +24,8 @@ module.exports = createSlice({
|
|||
name: "instances",
|
||||
initialState: {
|
||||
info: {},
|
||||
current: undefined
|
||||
},
|
||||
reducers: {
|
||||
setInstance: (state, {payload}) => {
|
||||
state.current = payload;
|
||||
},
|
||||
setInstanceInfo: (state, {payload}) => {
|
||||
let [key, info] = payload;
|
||||
state.info[key] = info;
|
||||
|
|
|
@ -23,13 +23,26 @@ const {createSlice} = require("@reduxjs/toolkit");
|
|||
module.exports = createSlice({
|
||||
name: "oauth",
|
||||
initialState: {
|
||||
loggedIn: false,
|
||||
registrations: {}
|
||||
loginState: 'none'
|
||||
},
|
||||
reducers: {
|
||||
setInstance: (state, {payload}) => {
|
||||
state.instance = payload;
|
||||
},
|
||||
setRegistration: (state, {payload}) => {
|
||||
let [key, info] = payload;
|
||||
state.instanceRegistration[key] = info;
|
||||
state.registration = payload;
|
||||
},
|
||||
setLoginState: (state, {payload}) => {
|
||||
state.loginState = payload;
|
||||
},
|
||||
login: (state, {payload}) => {
|
||||
state.token = `${payload.token_type} ${payload.access_token}`;
|
||||
state.loginState = "login";
|
||||
},
|
||||
remove: (state, {_payload}) => {
|
||||
delete state.token;
|
||||
delete state.registration;
|
||||
state.loginState = "none";
|
||||
}
|
||||
}
|
||||
});
|
32
web/source/settings-panel/redux/reducers/temporary.js
Normal file
32
web/source/settings-panel/redux/reducers/temporary.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
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 {createSlice} = require("@reduxjs/toolkit");
|
||||
|
||||
module.exports = createSlice({
|
||||
name: "temporary",
|
||||
initialState: {
|
||||
},
|
||||
reducers: {
|
||||
setStatus: function(state, {payload}) {
|
||||
state.status = payload;
|
||||
}
|
||||
}
|
||||
});
|
|
@ -31,6 +31,8 @@ section {
|
|||
#root {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr min(92%, 90ch) 1fr;
|
||||
width: 100vw;
|
||||
max-width: 100vw;
|
||||
|
||||
section.with-sidebar {
|
||||
border-left: none;
|
||||
|
@ -128,12 +130,17 @@ input, select, textarea {
|
|||
}
|
||||
|
||||
.error {
|
||||
background: $error2;
|
||||
border: 1px solid $error1;
|
||||
color: $error-fg;
|
||||
background: $error-bg;
|
||||
border: 0.02rem solid $error-fg;
|
||||
border-radius: $br;
|
||||
color: $error1;
|
||||
font-weight: bold;
|
||||
padding: 0.5rem;
|
||||
white-space: pre-wrap; // to show \n in json errors
|
||||
|
||||
a {
|
||||
color: $error-link;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: $bg;
|
||||
|
|
Loading…
Reference in a new issue