admin-status based routing

This commit is contained in:
f0x 2022-09-15 20:02:55 +02:00
parent 02ac28e832
commit 9fbe8f5cfd
13 changed files with 174 additions and 41 deletions

View file

@ -83,6 +83,11 @@ header, footer {
align-self: start;
}
header {
display: flex;
justify-content: center;
}
header a {
margin: 2rem;
/* background: $header-bg; */

View file

@ -1,6 +1,6 @@
{
"name": "gotosocial-frontend",
"version": "0.3.8",
"version": "0.5.0",
"description": "GoToSocial frontend sources",
"main": "index.js",
"author": "f0x",

View file

@ -18,6 +18,65 @@
"use strict";
module.exports = function Federation() {
return "federation";
const Promise = require("bluebird");
const React = require("react");
const Redux = require("react-redux");
const Submit = require("../components/submit");
const api = require("../lib/api");
const adminActions = require("../redux/reducers/instances").actions;
const {
TextInput,
TextArea,
File
} = require("../components/form-fields").formFields(adminActions.setAdminSettingsVal, (state) => state.instances.adminSettings);
module.exports = function AdminSettings() {
const dispatch = Redux.useDispatch();
const instance = Redux.useSelector(state => state.instances.adminSettings);
const [loaded, setLoaded] = React.useState(false);
const [errorMsg, setError] = React.useState("");
const [statusMsg, setStatus] = React.useState("");
React.useEffect(() => {
Promise.try(() => {
return dispatch(api.admin.fetchDomainBlocks());
}).then(() => {
setLoaded(true);
}).catch((e) => {
console.log(e);
});
}, []);
function submit() {
setStatus("PATCHing");
setError("");
return Promise.try(() => {
return dispatch(api.admin.updateInstance());
}).then(() => {
setStatus("Saved!");
}).catch((e) => {
setError(e.message);
setStatus("");
});
}
if (!loaded) {
return (
<div>
<h1>Federation</h1>
Loading instance blocks...
</div>
);
}
return (
<div>
<h1>Federation</h1>
</div>
);
};

View file

@ -71,7 +71,7 @@ function get(state, id) {
module.exports = {
formFields: function formFields(setter, selector) {
function FormField({type, id, name, className="", placeHolder="", fileType="", children=null, options={}}) {
function FormField({type, id, name, className="", placeHolder="", fileType="", children=null, options=null}) {
const dispatch = Redux.useDispatch();
let state = Redux.useSelector(selector);
let {
@ -111,7 +111,6 @@ module.exports = {
}
let label = <label htmlFor={id}>{name}</label>;
return (
<div className={`form-field ${type}`}>
{defaultLabel ? label : null}

View file

@ -28,6 +28,8 @@ const { PersistGate } = require("redux-persist/integration/react");
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");
@ -40,6 +42,7 @@ const nav = {
"Settings": require("./user/settings.js"),
},
"Admin": {
adminOnly: true,
"Instance Settings": require("./admin/settings.js"),
"Federation": require("./admin/federation.js"),
"Custom Emoji": require("./admin/emoji.js"),
@ -47,15 +50,16 @@ const nav = {
}
};
// Generate component tree from `nav` object once, as it won't change
const { sidebar, panelRouter } = require("./lib/generate-views")(nav);
const { sidebar, panelRouter } = require("./lib/get-views")(nav);
function App() {
const dispatch = Redux.useDispatch();
const { loginState } = Redux.useSelector((state) => state.oauth);
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);
const [errorMsg, setErrorMsg] = React.useState();
const [tokenChecked, setTokenChecked] = React.useState(false);
React.useEffect(() => {
if (loginState == "login" || loginState == "callback") {
@ -64,7 +68,7 @@ function App() {
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 {
@ -79,7 +83,13 @@ function App() {
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.message);
});
@ -97,7 +107,7 @@ function App() {
}
const LogoutElement = (
<button className="logout" onClick={() => {dispatch(api.oauth.logout());}}>
<button className="logout" onClick={() => { dispatch(api.oauth.logout()); }}>
Log out
</button>
);
@ -112,27 +122,29 @@ function App() {
return (
<>
<div className="sidebar">
{sidebar}
{sidebar.all}
{isAdmin && sidebar.admin}
{LogoutElement}
</div>
<section className="with-sidebar">
{ErrorElement}
<Switch>
<Route path="/settings">
{panelRouter.all}
{isAdmin && panelRouter.admin}
<Route> {/* default route */}
<Redirect to="/settings/user" />
</Route>
{panelRouter}
</Switch>
</section>
</>
);
} else if (loginState == "none") {
return (
<Login error={ErrorElement}/>
<Login error={ErrorElement} />
);
} else {
let status;
if (loginState == "login") {
status = "Verifying stored login...";
} else if (loginState == "callback") {

View file

@ -39,6 +39,14 @@ module.exports = function ({ apiCall, getChanges }) {
return dispatch(instance.setInstanceInfo(data));
});
};
},
fetchDomainBlocks: function fetchDomainBlocks() {
return function (dispatch, _getState) {
return Promise.try(() => {
return dispatch(apiCall("GET", "/api/v1/admin/domain_blocks"));
});
};
}
};
};

View file

@ -22,7 +22,7 @@ const Promise = require("bluebird");
const { isPlainObject } = require("is-plain-object");
const d = require("dotty");
const { APIError } = require("../errors");
const { APIError, AuthenticationError } = require("../errors");
const { setInstanceInfo, setNamedInstanceInfo } = require("../../redux/reducers/instances").actions;
const oauth = require("../../redux/reducers/oauth").actions;
@ -83,12 +83,12 @@ function apiCall(method, route, payload, type = "json") {
return Promise.all([res, json]);
}).then(([res, json]) => {
if (!res.ok) {
if (auth != undefined && res.status == 401) {
if (auth != undefined && (res.status == 401 || res.status == 403)) {
// stored access token is invalid
dispatch(oauth.remove());
throw new APIError("Stored OAUTH login was no longer valid, please log in again.");
throw new AuthenticationError("401: Authentication error", {json, status: res.status});
} else {
throw new APIError(json.error, { json });
}
throw new APIError(json.error, { json });
} else {
return json;
}

View file

@ -20,13 +20,13 @@
const Promise = require("bluebird");
const { OAUTHError } = require("../errors");
const { OAUTHError, AuthenticationError } = require("../errors");
const oauth = require("../../redux/reducers/oauth").actions;
const temporary = require("../../redux/reducers/temporary").actions;
const user = require("../../redux/reducers/user").actions;
module.exports = function oauthAPI({apiCall, getCurrentUrl}) {
module.exports = function oauthAPI({ apiCall, getCurrentUrl }) {
return {
register: function register(scopes = []) {
@ -44,36 +44,36 @@ module.exports = function oauthAPI({apiCall, getCurrentUrl}) {
});
};
},
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,
@ -88,11 +88,35 @@ module.exports = function oauthAPI({apiCall, getCurrentUrl}) {
});
};
},
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(() => {
return dispatch(oauth.setAdmin(true));
}).catch(AuthenticationError, () => {
return dispatch(oauth.setAdmin(false));
}).catch((e) => {
console.log("caught", e, e instanceof AuthenticationError);
});
};
},
logout: function logout() {
return function (dispatch, _getState) {
// TODO: GoToSocial does not have a logout API route yet
return dispatch(oauth.remove());
};
}

View file

@ -23,4 +23,5 @@ const createError = require("create-error");
module.exports = {
APIError: createError("APIError"),
OAUTHError: createError("OAUTHError"),
AuthenticationError: createError("AuthenticationError"),
};

View file

@ -19,6 +19,7 @@
"use strict";
const React = require("react");
const Redux = require("react-redux");
const { Link, Route, Switch, Redirect } = require("wouter");
const { ErrorBoundary } = require("react-error-boundary");
@ -29,11 +30,29 @@ function urlSafe(str) {
return str.toLowerCase().replace(/\s+/g, "-");
}
module.exports = function generateViews(struct) {
const sidebar = [];
const panelRouter = [];
module.exports = function getViews(struct) {
const sidebar = {
all: [],
admin: [],
};
const panelRouter = {
all: [],
admin: [],
};
Object.entries(struct).forEach(([name, entries]) => {
let sidebarEl = sidebar.all;
let panelRouterEl = panelRouter.all;
if (entries.adminOnly) {
sidebarEl = sidebar.admin;
panelRouterEl = panelRouter.admin;
delete entries.adminOnly;
}
console.log(name, entries);
let base = `/settings/${urlSafe(name)}`;
let links = [];
@ -62,14 +81,14 @@ module.exports = function generateViews(struct) {
);
});
panelRouter.push(
panelRouterEl.push(
<Route key={base} path={base}>
<Redirect to={firstRoute} />
</Route>
);
let childrenPath = `${base}/:section`;
panelRouter.push(
panelRouterEl.push(
<Route key={childrenPath} path={childrenPath}>
<Switch>
{routes}
@ -77,7 +96,7 @@ module.exports = function generateViews(struct) {
</Route>
);
sidebar.push(
sidebarEl.push(
<React.Fragment key={name}>
<Link href={firstRoute}>
<a>

View file

@ -23,7 +23,7 @@ const {createSlice} = require("@reduxjs/toolkit");
module.exports = createSlice({
name: "oauth",
initialState: {
loginState: 'none'
loginState: 'none',
},
reducers: {
setInstance: (state, {payload}) => {
@ -42,7 +42,11 @@ module.exports = createSlice({
remove: (state, {_payload}) => {
delete state.token;
delete state.registration;
delete state.isAdmin;
state.loginState = "none";
},
setAdmin: (state, {payload}) => {
state.isAdmin = payload;
}
}
});

View file

@ -51,6 +51,7 @@ section {
border-bottom-right-radius: 0;
display: flex;
flex-direction: column;
min-width: 12rem;
a {
text-decoration: none;

View file

@ -55,8 +55,9 @@ module.exports = function UserSettings() {
return (
<div className="user-settings">
<h1>Post settings</h1>
<Select id="language" name="Default post language">
<Select id="language" name="Default post language" options={
<Languages/>
}>
</Select>
<Select id="privacy" name="Default post privacy" options={
<>