refactor authentication with RTK Query
This commit is contained in:
parent
f5bc524f1e
commit
a1d184eac4
|
@ -66,6 +66,7 @@ skulk({
|
|||
],
|
||||
},
|
||||
settings: {
|
||||
debug: false,
|
||||
entryFile: "settings",
|
||||
outputFile: "settings.js",
|
||||
prodCfg: prodCfg,
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021-2023 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 query = require("../../lib/query");
|
||||
|
||||
const Login = require("./login");
|
||||
const Loading = require("../loading");
|
||||
const { Error } = require("../error");
|
||||
|
||||
module.exports = function Authorization({ App }) {
|
||||
const loginState = Redux.useSelector((state) => state.oauth.loginState);
|
||||
const { isLoading, isSuccess, data: account, error } = query.useVerifyCredentialsQuery();
|
||||
|
||||
let showLogin = true;
|
||||
let content = null;
|
||||
|
||||
if (isLoading && loginState != "none") {
|
||||
showLogin = false;
|
||||
|
||||
let loadingInfo;
|
||||
if (loginState == "callback") {
|
||||
loadingInfo = "Processing OAUTH callback.";
|
||||
} else if (loginState == "login") {
|
||||
loadingInfo = "Verifying stored login.";
|
||||
}
|
||||
|
||||
content = (
|
||||
<div>
|
||||
<Loading /> {loadingInfo}
|
||||
</div>
|
||||
);
|
||||
} else if (error != undefined) {
|
||||
content = (
|
||||
<Error error={error} />
|
||||
);
|
||||
}
|
||||
|
||||
if (loginState == "login" && isSuccess) {
|
||||
return <App account={account} />;
|
||||
} else {
|
||||
return (
|
||||
<section className="oauth">
|
||||
<h1>GoToSocial Settings</h1>
|
||||
{content}
|
||||
{showLogin && <Login />}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021-2023 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 query = require("../../lib/query");
|
||||
const { useTextInput, useValue } = require("../../lib/form");
|
||||
const useFormSubmit = require("../../lib/form/submit");
|
||||
const { TextInput } = require("../form/inputs");
|
||||
const MutationButton = require("../form/mutation-button");
|
||||
const Loading = require("../loading");
|
||||
|
||||
module.exports = function Login({ }) {
|
||||
const form = {
|
||||
instance: useTextInput("instance", {
|
||||
defaultValue: window.location.origin,
|
||||
validator: (value) => {
|
||||
return "";
|
||||
}
|
||||
}),
|
||||
scopes: useValue("scopes", "user admin")
|
||||
};
|
||||
|
||||
const [formSubmit, result] = useFormSubmit(
|
||||
form,
|
||||
query.useAuthorizeFlowMutation(),
|
||||
{ changedOnly: false }
|
||||
);
|
||||
|
||||
if (result.isLoading) {
|
||||
return (
|
||||
<div>
|
||||
<Loading /> Checking instance.
|
||||
</div>
|
||||
);
|
||||
} else if (result.isSuccess) {
|
||||
return (
|
||||
<div>
|
||||
<Loading /> Redirecting to instance authorization page.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={formSubmit}>
|
||||
<TextInput
|
||||
field={form.instance}
|
||||
label="Instance"
|
||||
/>
|
||||
<MutationButton label="Login" result={result} />
|
||||
</form>
|
||||
);
|
||||
};
|
|
@ -51,6 +51,11 @@ function Error({ error }) {
|
|||
if (error.status) {
|
||||
message = (<>
|
||||
<b>{error.status}:</b> {error.data.error}
|
||||
{error.data.error_description &&
|
||||
<p>
|
||||
{error.data.error_description}
|
||||
</p>
|
||||
}
|
||||
</>);
|
||||
} else {
|
||||
message = error.data.error;
|
||||
|
@ -59,6 +64,10 @@ function Error({ error }) {
|
|||
message = (<>
|
||||
<b>{error.type && error.name}:</b> {error.message}
|
||||
</>);
|
||||
} else if (error.status && typeof error.error == "string") {
|
||||
message = (<>
|
||||
<b>{error.status}:</b> {error.error}
|
||||
</>);
|
||||
} else {
|
||||
message = error.message ?? error;
|
||||
}
|
||||
|
|
|
@ -19,10 +19,15 @@
|
|||
"use strict";
|
||||
|
||||
const React = require("react");
|
||||
const Redux = require("react-redux");
|
||||
|
||||
const query = require("../lib/query");
|
||||
|
||||
module.exports = function FakeToot({ children }) {
|
||||
const account = Redux.useSelector((state) => state.user.profile);
|
||||
const { data: account = {
|
||||
avatar: "/assets/default_avatars/GoToSocial_icon1.png",
|
||||
display_name: "",
|
||||
username: ""
|
||||
} } = query.useVerifyCredentialsQuery();
|
||||
|
||||
return (
|
||||
<div className="toot expanded">
|
||||
|
|
|
@ -1,96 +0,0 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021-2023 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 Redux = require("react-redux");
|
||||
|
||||
const { setInstance } = require("../redux/reducers/oauth").actions;
|
||||
const api = require("../lib/api");
|
||||
const { Error } = require("./error");
|
||||
|
||||
module.exports = function Login({ error }) {
|
||||
const dispatch = Redux.useDispatch();
|
||||
const [instanceField, setInstanceField] = React.useState("");
|
||||
const [loginError, setLoginError] = React.useState();
|
||||
const instanceFieldRef = React.useRef("");
|
||||
|
||||
React.useEffect(() => {
|
||||
// check if current domain runs an instance
|
||||
let currentDomain = window.location.origin;
|
||||
Promise.try(() => {
|
||||
return dispatch(api.instance.fetchWithoutStore(currentDomain));
|
||||
}).then(() => {
|
||||
if (instanceFieldRef.current.length == 0) { // user hasn't started typing yet
|
||||
dispatch(setInstance(currentDomain));
|
||||
instanceFieldRef.current = currentDomain;
|
||||
setInstanceField(currentDomain);
|
||||
}
|
||||
}).catch((e) => {
|
||||
console.log("Current domain does not host a valid instance: ", e);
|
||||
});
|
||||
}, []);
|
||||
|
||||
function tryInstance() {
|
||||
let domain = instanceFieldRef.current;
|
||||
Promise.try(() => {
|
||||
return dispatch(api.instance.fetchWithoutStore(domain)).catch((e) => {
|
||||
// TODO: clearer error messages for common errors
|
||||
console.log(e);
|
||||
throw e;
|
||||
});
|
||||
}).then(() => {
|
||||
dispatch(setInstance(domain));
|
||||
|
||||
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) => {
|
||||
setLoginError(e);
|
||||
});
|
||||
}
|
||||
|
||||
function updateInstanceField(e) {
|
||||
if (e.key == "Enter") {
|
||||
tryInstance(instanceField);
|
||||
} else {
|
||||
setInstanceField(e.target.value);
|
||||
instanceFieldRef.current = e.target.value;
|
||||
}
|
||||
}
|
||||
|
||||
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" />
|
||||
{loginError &&
|
||||
<Error error={loginError} />
|
||||
}
|
||||
<button onClick={tryInstance}>Authenticate</button>
|
||||
</form>
|
||||
</section>
|
||||
);
|
||||
};
|
|
@ -1,35 +0,0 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021-2023 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");
|
||||
|
||||
module.exports = function Submit({ onClick, label, errorMsg, statusMsg }) {
|
||||
return (
|
||||
<div className="messagebutton">
|
||||
<button type="submit" onClick={onClick}>{label}</button>
|
||||
{errorMsg.length > 0 &&
|
||||
<div className="error accent">{errorMsg}</div>
|
||||
}
|
||||
{statusMsg.length > 0 &&
|
||||
<div className="accent">{statusMsg}</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -18,22 +18,17 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
const Promise = require("bluebird");
|
||||
const React = require("react");
|
||||
const ReactDom = require("react-dom/client");
|
||||
const Redux = require("react-redux");
|
||||
const { Switch, Route, Redirect } = require("wouter");
|
||||
const { Provider } = require("react-redux");
|
||||
const { PersistGate } = require("redux-persist/integration/react");
|
||||
const { Switch, Route, Redirect } = require("wouter");
|
||||
|
||||
const query = require("./lib/query");
|
||||
|
||||
const { store, persistor } = require("./redux");
|
||||
const api = require("./lib/api");
|
||||
const oauth = require("./redux/reducers/oauth").actions;
|
||||
const { AuthenticationError } = require("./lib/errors");
|
||||
|
||||
const Login = require("./components/login");
|
||||
const AuthorizationGate = require("./components/authorization");
|
||||
const Loading = require("./components/loading");
|
||||
const { Error } = require("./components/error");
|
||||
|
||||
require("./style.css");
|
||||
|
||||
|
@ -58,118 +53,37 @@ const nav = {
|
|||
|
||||
const { sidebar, panelRouter } = require("./lib/get-views")(nav);
|
||||
|
||||
function App() {
|
||||
const dispatch = Redux.useDispatch();
|
||||
function App({ account }) {
|
||||
const isAdmin = account.role == "admin";
|
||||
const [logoutQuery] = query.useLogoutMutation();
|
||||
|
||||
const { loginState, isAdmin } = Redux.useSelector((state) => state.oauth);
|
||||
const reduxTempStatus = Redux.useSelector((state) => state.temporary.status);
|
||||
|
||||
const [errorMsg, setErrorMsg] = React.useState();
|
||||
const [tokenChecked, setTokenChecked] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (loginState == "login" || loginState == "callback") {
|
||||
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");
|
||||
|
||||
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.tokenize(code));
|
||||
}
|
||||
}
|
||||
}).then(() => {
|
||||
// Fetch current instance info
|
||||
return dispatch(api.instance.fetch());
|
||||
}).then(() => {
|
||||
// Check currently stored auth token for validity if available
|
||||
return dispatch(api.user.fetchAccount());
|
||||
}).then(() => {
|
||||
setTokenChecked(true);
|
||||
|
||||
return dispatch(api.oauth.checkIfAdmin());
|
||||
}).catch((e) => {
|
||||
if (e instanceof AuthenticationError) {
|
||||
dispatch(oauth.remove());
|
||||
e.message = "Stored OAUTH token no longer valid, please log in again.";
|
||||
}
|
||||
setErrorMsg(e);
|
||||
console.error(e);
|
||||
});
|
||||
}
|
||||
}, [loginState, dispatch]);
|
||||
|
||||
let ErrorElement = null;
|
||||
if (errorMsg != undefined) {
|
||||
ErrorElement = <Error error={errorMsg} />;
|
||||
}
|
||||
|
||||
const LogoutElement = (
|
||||
<button className="logout" onClick={() => { dispatch(api.oauth.logout()); }}>
|
||||
Log out
|
||||
</button>
|
||||
return (
|
||||
<>
|
||||
<div className="sidebar">
|
||||
{sidebar.all}
|
||||
{isAdmin && sidebar.admin}
|
||||
<button className="logout" onClick={logoutQuery}>
|
||||
Log out
|
||||
</button>
|
||||
</div>
|
||||
<section className="with-sidebar">
|
||||
<Switch>
|
||||
{panelRouter.all}
|
||||
{isAdmin && panelRouter.admin}
|
||||
<Route>
|
||||
<Redirect to="/settings/user" />
|
||||
</Route>
|
||||
</Switch>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
|
||||
if (reduxTempStatus != undefined) {
|
||||
return (
|
||||
<section>
|
||||
{reduxTempStatus}
|
||||
</section>
|
||||
);
|
||||
} else if (tokenChecked && loginState == "login") {
|
||||
return (
|
||||
<>
|
||||
<div className="sidebar">
|
||||
{sidebar.all}
|
||||
{isAdmin && sidebar.admin}
|
||||
{LogoutElement}
|
||||
</div>
|
||||
<section className="with-sidebar">
|
||||
{ErrorElement}
|
||||
<Switch>
|
||||
{panelRouter.all}
|
||||
{isAdmin && panelRouter.admin}
|
||||
<Route> {/* default route */}
|
||||
<Redirect to="/settings/user" />
|
||||
</Route>
|
||||
</Switch>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
} else if (loginState == "none") {
|
||||
return (
|
||||
<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() {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<PersistGate loading={<section><Loading /></section>} persistor={persistor}>
|
||||
<App />
|
||||
<AuthorizationGate App={App} />
|
||||
</PersistGate>
|
||||
</Provider>
|
||||
);
|
||||
|
|
|
@ -1,192 +0,0 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021-2023 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;
|
||||
|
||||
function apiCall(method, route, payload, type = "json") {
|
||||
return function (dispatch, getState) {
|
||||
const state = getState();
|
||||
let base = state.oauth.instance;
|
||||
let auth = state.oauth.token;
|
||||
|
||||
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") {
|
||||
body = convertToForm(payload);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
Takes an object with (nested) keys, and transforms it into
|
||||
a FormData object to be sent over the API
|
||||
*/
|
||||
function convertToForm(payload) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
return formData;
|
||||
}
|
||||
|
||||
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() {
|
||||
let [pre, _past] = window.location.pathname.split("/settings");
|
||||
return `${window.location.origin}${pre}/settings`;
|
||||
}
|
||||
|
||||
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),
|
||||
apiCall,
|
||||
convertToForm,
|
||||
getChanges
|
||||
};
|
|
@ -1,123 +0,0 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021-2023 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;
|
||||
|
||||
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
|
||||
if (state.user.profile.role == "admin") {
|
||||
dispatch(oauth.setAdmin(true));
|
||||
return true;
|
||||
}
|
||||
|
||||
// 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(() => {
|
||||
return dispatch(oauth.setAdmin(true));
|
||||
}).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());
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
|
@ -1,37 +0,0 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021-2023 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 }) {
|
||||
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));
|
||||
});
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
|
@ -1,27 +0,0 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021-2023 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"),
|
||||
};
|
|
@ -22,7 +22,11 @@ const Promise = require("bluebird");
|
|||
const React = require("react");
|
||||
const syncpipe = require("syncpipe");
|
||||
|
||||
module.exports = function useFormSubmit(form, [mutationQuery, result], { changedOnly = true } = {}) {
|
||||
module.exports = function useFormSubmit(form, mutationQuery, { changedOnly = true } = {}) {
|
||||
if (!Array.isArray(mutationQuery)) {
|
||||
throw new ("useFormSubmit: mutationQuery was not an Array. Is a valid useMutation RTK Query provided?");
|
||||
}
|
||||
const [runMutation, result] = mutationQuery;
|
||||
const [usedAction, setUsedAction] = React.useState();
|
||||
return [
|
||||
function submitForm(e) {
|
||||
|
@ -62,7 +66,7 @@ module.exports = function useFormSubmit(form, [mutationQuery, result], { changed
|
|||
mutationData.action = action;
|
||||
|
||||
return Promise.try(() => {
|
||||
return mutationQuery(mutationData);
|
||||
return runMutation(mutationData);
|
||||
}).then((res) => {
|
||||
if (res.error == undefined) {
|
||||
updatedFields.forEach((field) => {
|
||||
|
|
|
@ -37,7 +37,7 @@ module.exports = function useTextInput({ name, Name }, { validator, defaultValue
|
|||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if (validator) {
|
||||
if (validator && textRef.current) {
|
||||
let res = validator(text);
|
||||
setValid(res == "");
|
||||
textRef.current.setCustomValidity(res);
|
||||
|
|
|
@ -20,10 +20,9 @@
|
|||
|
||||
const Promise = require("bluebird");
|
||||
|
||||
const { unwrapRes } = require("./lib");
|
||||
const base = require("./base");
|
||||
const { unwrapRes } = require("../lib");
|
||||
|
||||
const endpoints = (build) => ({
|
||||
module.exports = (build) => ({
|
||||
getAllEmoji: build.query({
|
||||
query: (params = {}) => ({
|
||||
url: "/api/v1/admin/custom_emojis",
|
||||
|
@ -194,6 +193,4 @@ function emojiFromSearchResult(searchRes) {
|
|||
domain: (new URL(data.url)).host, // to get WEB_DOMAIN, see https://github.com/superseriousbusiness/gotosocial/issues/1225
|
||||
list: data.emojis
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = base.injectEndpoints({ endpoints });
|
||||
}
|
|
@ -77,7 +77,8 @@ const endpoints = (build) => ({
|
|||
}
|
||||
})
|
||||
}),
|
||||
...require("./import-export")(build)
|
||||
...require("./import-export")(build),
|
||||
...require("./custom-emoji")(build)
|
||||
});
|
||||
|
||||
module.exports = base.injectEndpoints({ endpoints });
|
|
@ -51,13 +51,23 @@ function instanceBasedQuery(args, api, extraOptions) {
|
|||
headers.set("Accept", "application/json");
|
||||
return headers;
|
||||
},
|
||||
})(args, api, extraOptions);
|
||||
})(args, api, extraOptions).then((res) => {
|
||||
if (res.error != undefined) {
|
||||
const { error } = res;
|
||||
// if (error.status == 401 || error.status == 403) {
|
||||
|
||||
// }
|
||||
return res;
|
||||
} else {
|
||||
return res;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = createApi({
|
||||
reducerPath: "api",
|
||||
baseQuery: instanceBasedQuery,
|
||||
tagTypes: ["Emojis", "User"],
|
||||
tagTypes: ["Auth"],
|
||||
endpoints: (build) => ({
|
||||
instance: build.query({
|
||||
query: () => ({
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
|
||||
module.exports = {
|
||||
...require("./base"),
|
||||
...require("./custom-emoji.js"),
|
||||
...require("./oauth"),
|
||||
...require("./user"),
|
||||
...require("./admin")
|
||||
};
|
|
@ -0,0 +1,159 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021-2023 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 base = require("./base");
|
||||
const { unwrapRes } = require("./lib");
|
||||
const oauth = require("../../redux/oauth").actions;
|
||||
|
||||
function getSettingsURL() {
|
||||
/* needed in case the settings interface isn't hosted at /settings but
|
||||
some subpath like /gotosocial/settings. Other parts of the code don't
|
||||
take this into account yet so mostly future-proofing.
|
||||
|
||||
Also drops anything past /settings/, because authorization urls that are too long
|
||||
get rejected by GTS.
|
||||
*/
|
||||
let [pre, _past] = window.location.pathname.split("/settings");
|
||||
return `${window.location.origin}${pre}/settings`;
|
||||
}
|
||||
|
||||
const SETTINGS_URL = getSettingsURL();
|
||||
|
||||
const endpoints = (build) => ({
|
||||
verifyCredentials: build.query({
|
||||
providesTags: (_res, error) =>
|
||||
error == undefined
|
||||
? ["Auth"]
|
||||
: [],
|
||||
queryFn: (_arg, api, _extraOpts, baseQuery) => {
|
||||
const state = api.getState();
|
||||
|
||||
return Promise.try(() => {
|
||||
// Process callback code first, if available
|
||||
if (state.oauth.loginState == "callback") {
|
||||
let urlParams = new URLSearchParams(window.location.search);
|
||||
let code = urlParams.get("code");
|
||||
|
||||
if (code == undefined) {
|
||||
throw {
|
||||
message: "Waiting for callback, but no ?code= provided in url."
|
||||
};
|
||||
} else {
|
||||
let app = state.oauth.registration;
|
||||
|
||||
if (app == undefined || app.client_id == undefined) {
|
||||
throw {
|
||||
message: "No stored registration data, can't finish login flow. Please try again:"
|
||||
};
|
||||
}
|
||||
|
||||
return baseQuery({
|
||||
method: "POST",
|
||||
url: "/oauth/token",
|
||||
body: {
|
||||
client_id: app.client_id,
|
||||
client_secret: app.client_secret,
|
||||
redirect_uri: SETTINGS_URL,
|
||||
grant_type: "authorization_code",
|
||||
code: code
|
||||
}
|
||||
}).then(unwrapRes).then((token) => {
|
||||
// remove ?code= from url
|
||||
window.history.replaceState({}, document.title, window.location.pathname);
|
||||
api.dispatch(oauth.setToken(token));
|
||||
});
|
||||
}
|
||||
}
|
||||
}).then(() => {
|
||||
return baseQuery({
|
||||
url: `/api/v1/accounts/verify_credentials`
|
||||
});
|
||||
}).catch((e) => {
|
||||
return { error: e };
|
||||
});
|
||||
}
|
||||
}),
|
||||
authorizeFlow: build.mutation({
|
||||
queryFn: (formData, api, _extraOpts, baseQuery) => {
|
||||
let instance;
|
||||
const state = api.getState();
|
||||
|
||||
return Promise.try(() => {
|
||||
if (!formData.instance.startsWith("http")) {
|
||||
formData.instance = `https://${formData.instance}`;
|
||||
}
|
||||
instance = new URL(formData.instance).origin;
|
||||
|
||||
const stored = state.oauth.instance;
|
||||
if (stored?.instance == instance && stored.registration) {
|
||||
return stored.registration;
|
||||
}
|
||||
|
||||
return baseQuery({
|
||||
method: "POST",
|
||||
baseUrl: instance,
|
||||
url: "/api/v1/apps",
|
||||
body: {
|
||||
client_name: "GoToSocial Settings",
|
||||
scopes: formData.scopes,
|
||||
redirect_uris: SETTINGS_URL,
|
||||
website: SETTINGS_URL
|
||||
}
|
||||
}).then(unwrapRes).then((app) => {
|
||||
app.scopes = formData.scopes;
|
||||
|
||||
api.dispatch(oauth.setInstance({
|
||||
instance: instance,
|
||||
registration: app,
|
||||
loginState: "callback"
|
||||
}));
|
||||
|
||||
return app;
|
||||
});
|
||||
}).then((app) => {
|
||||
let url = new URL(instance);
|
||||
url.pathname = "/oauth/authorize";
|
||||
url.searchParams.set("client_id", app.client_id);
|
||||
url.searchParams.set("redirect_uri", SETTINGS_URL);
|
||||
url.searchParams.set("response_type", "code");
|
||||
url.searchParams.set("scope", app.scopes);
|
||||
|
||||
let redirectURL = url.toString();
|
||||
console.log("OAUTH redirect to:", redirectURL);
|
||||
window.location.assign(redirectURL);
|
||||
|
||||
return { data: null };
|
||||
}).catch((e) => {
|
||||
return { error: e };
|
||||
});
|
||||
},
|
||||
}),
|
||||
logout: build.mutation({
|
||||
queryFn: (_arg, api) => {
|
||||
api.dispatch(oauth.remove());
|
||||
return { data: null };
|
||||
},
|
||||
invalidatesTags: ["Auth"]
|
||||
})
|
||||
});
|
||||
|
||||
module.exports = base.injectEndpoints({ endpoints });
|
|
@ -22,11 +22,6 @@ const { replaceCacheOnMutation } = require("./lib");
|
|||
const base = require("./base");
|
||||
|
||||
const endpoints = (build) => ({
|
||||
verifyCredentials: build.query({
|
||||
query: () => ({
|
||||
url: `/api/v1/accounts/verify_credentials`
|
||||
})
|
||||
}),
|
||||
updateCredentials: build.mutation({
|
||||
query: (formData) => ({
|
||||
method: "PATCH",
|
||||
|
|
|
@ -34,17 +34,14 @@ const {
|
|||
const query = require("../lib/query/base");
|
||||
|
||||
const combinedReducers = combineReducers({
|
||||
oauth: require("./reducers/oauth").reducer,
|
||||
instances: require("./reducers/instances").reducer,
|
||||
temporary: require("./reducers/temporary").reducer,
|
||||
user: require("./reducers/user").reducer,
|
||||
oauth: require("./oauth").reducer,
|
||||
[query.reducerPath]: query.reducer
|
||||
});
|
||||
|
||||
const persistedReducer = persistReducer({
|
||||
key: "gotosocial-settings",
|
||||
storage: require("redux-persist/lib/storage").default,
|
||||
stateReconciler: require("redux-persist/lib/stateReconciler/autoMergeLevel2").default,
|
||||
stateReconciler: require("redux-persist/lib/stateReconciler/autoMergeLevel1").default,
|
||||
whitelist: ["oauth"],
|
||||
}, combinedReducers);
|
||||
|
||||
|
@ -53,7 +50,7 @@ const store = configureStore({
|
|||
middleware: (getDefaultMiddleware) => {
|
||||
return getDefaultMiddleware({
|
||||
serializableCheck: {
|
||||
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER, "temporary/setScrollElement"]
|
||||
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER]
|
||||
}
|
||||
}).concat(query.middleware);
|
||||
}
|
||||
|
|
|
@ -23,30 +23,26 @@ const { createSlice } = require("@reduxjs/toolkit");
|
|||
module.exports = createSlice({
|
||||
name: "oauth",
|
||||
initialState: {
|
||||
loginState: 'none',
|
||||
loginState: 'none'
|
||||
},
|
||||
reducers: {
|
||||
setInstance: (state, { payload }) => {
|
||||
state.instance = payload;
|
||||
return {
|
||||
...state,
|
||||
...payload /* overrides instance, registration keys */
|
||||
};
|
||||
},
|
||||
setRegistration: (state, { payload }) => {
|
||||
state.registration = payload;
|
||||
authorize: (state) => {
|
||||
state.loginState = "callback";
|
||||
},
|
||||
setLoginState: (state, { payload }) => {
|
||||
state.loginState = payload;
|
||||
},
|
||||
login: (state, { payload }) => {
|
||||
setToken: (state, { payload }) => {
|
||||
state.token = `${payload.token_type} ${payload.access_token}`;
|
||||
state.loginState = "login";
|
||||
},
|
||||
remove: (state, { _payload }) => {
|
||||
delete state.token;
|
||||
delete state.registration;
|
||||
delete state.isAdmin;
|
||||
state.loginState = "none";
|
||||
},
|
||||
setAdmin: (state, { payload }) => {
|
||||
state.isAdmin = payload;
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,42 +0,0 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021-2023 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");
|
||||
const d = require("dotty");
|
||||
|
||||
module.exports = createSlice({
|
||||
name: "instances",
|
||||
initialState: {
|
||||
info: {},
|
||||
},
|
||||
reducers: {
|
||||
setNamedInstanceInfo: (state, { payload }) => {
|
||||
let [key, info] = payload;
|
||||
state.info[key] = info;
|
||||
},
|
||||
setInstanceInfo: (state, { payload }) => {
|
||||
state.current = payload;
|
||||
state.adminSettings = payload;
|
||||
},
|
||||
setAdminSettingsVal: (state, { payload: [key, val] }) => {
|
||||
d.put(state.adminSettings, key, val);
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,32 +0,0 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021-2023 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;
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,43 +0,0 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021-2023 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: "user",
|
||||
initialState: {
|
||||
profile: {},
|
||||
settings: {}
|
||||
},
|
||||
reducers: {
|
||||
setAccount: (state, { payload }) => {
|
||||
payload.source = payload.source ?? {};
|
||||
payload.source.language = payload.source.language.toUpperCase() ?? "EN";
|
||||
payload.source.status_format = payload.source.status_format ?? "plain";
|
||||
payload.source.sensitive = payload.source.sensitive ?? false;
|
||||
|
||||
state.profile = payload;
|
||||
// /user/settings only needs a copy of the 'source' obj
|
||||
state.settings = {
|
||||
source: payload.source
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
|
@ -64,6 +64,11 @@ function UserProfileForm({ data: profile }) {
|
|||
- string custom_css (if enabled)
|
||||
*/
|
||||
|
||||
const { data: instance, isLoading: isLoadingInstance } = query.useInstanceQuery();
|
||||
const allowCustomCSS = React.useMemo(() => {
|
||||
return instance?.configuration?.accounts?.allow_custom_css === true;
|
||||
}, [instance]);
|
||||
|
||||
const form = {
|
||||
avatar: useFileInput("avatar", { withPreview: true }),
|
||||
header: useFileInput("header", { withPreview: true }),
|
||||
|
@ -75,7 +80,6 @@ function UserProfileForm({ data: profile }) {
|
|||
enableRSS: useBoolInput("enable_rss", { defaultValue: profile.enable_rss }),
|
||||
};
|
||||
|
||||
const allowCustomCSS = Redux.useSelector(state => state.instances.current.configuration.accounts.allow_custom_css);
|
||||
const [submitForm, result] = useFormSubmit(form, query.useUpdateCredentialsMutation());
|
||||
|
||||
return (
|
||||
|
|
|
@ -48,7 +48,8 @@ module.exports = function UserSettings() {
|
|||
);
|
||||
};
|
||||
|
||||
function UserSettingsForm({ data: { source } }) {
|
||||
function UserSettingsForm({ data }) {
|
||||
const { source } = data;
|
||||
/* form keys
|
||||
- string source[privacy]
|
||||
- bool source[sensitive]
|
||||
|
@ -59,7 +60,7 @@ function UserSettingsForm({ data: { source } }) {
|
|||
const form = {
|
||||
defaultPrivacy: useTextInput("source[privacy]", { defaultValue: source.privacy ?? "unlisted" }),
|
||||
isSensitive: useBoolInput("source[sensitive]", { defaultValue: source.sensitive }),
|
||||
language: useTextInput("source[language]", { defaultValue: source.language ?? "EN" }),
|
||||
language: useTextInput("source[language]", { defaultValue: source.language?.toUpperCase() ?? "EN" }),
|
||||
format: useTextInput("source[status_format]", { defaultValue: source.status_format ?? "plain" }),
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in New Issue