diff --git a/web/source/css/base.css b/web/source/css/base.css index 98c16acdd..ddca1efa8 100644 --- a/web/source/css/base.css +++ b/web/source/css/base.css @@ -83,6 +83,11 @@ header, footer { align-self: start; } +header { + display: flex; + justify-content: center; +} + header a { margin: 2rem; /* background: $header-bg; */ diff --git a/web/source/package.json b/web/source/package.json index 412c92329..8b0bde878 100644 --- a/web/source/package.json +++ b/web/source/package.json @@ -1,6 +1,6 @@ { "name": "gotosocial-frontend", - "version": "0.3.8", + "version": "0.5.0", "description": "GoToSocial frontend sources", "main": "index.js", "author": "f0x", diff --git a/web/source/settings-panel/admin/federation.js b/web/source/settings-panel/admin/federation.js index 4c97ebb8e..6898de602 100644 --- a/web/source/settings-panel/admin/federation.js +++ b/web/source/settings-panel/admin/federation.js @@ -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 ( +
+

Federation

+ Loading instance blocks... +
+ ); + } + + return ( +
+

Federation

+
+ ); }; \ No newline at end of file diff --git a/web/source/settings-panel/components/form-fields.jsx b/web/source/settings-panel/components/form-fields.jsx index 30f209f13..67c6b883a 100644 --- a/web/source/settings-panel/components/form-fields.jsx +++ b/web/source/settings-panel/components/form-fields.jsx @@ -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 = ; - return (
{defaultLabel ? label : null} diff --git a/web/source/settings-panel/index.js b/web/source/settings-panel/index.js index be0af56e7..8083807bb 100644 --- a/web/source/settings-panel/index.js +++ b/web/source/settings-panel/index.js @@ -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 = ( - ); @@ -112,27 +122,29 @@ function App() { return ( <>
- {sidebar} + {sidebar.all} + {isAdmin && sidebar.admin} {LogoutElement}
{ErrorElement} - + {panelRouter.all} + {isAdmin && panelRouter.admin} + {/* default route */} - {panelRouter}
); } else if (loginState == "none") { return ( - + ); } else { let status; - + if (loginState == "login") { status = "Verifying stored login..."; } else if (loginState == "callback") { diff --git a/web/source/settings-panel/lib/api/admin.js b/web/source/settings-panel/lib/api/admin.js index 7c4d3f94c..cfc243fd0 100644 --- a/web/source/settings-panel/lib/api/admin.js +++ b/web/source/settings-panel/lib/api/admin.js @@ -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")); + }); + }; } }; }; \ No newline at end of file diff --git a/web/source/settings-panel/lib/api/index.js b/web/source/settings-panel/lib/api/index.js index 1624c375a..9b2e6e5fe 100644 --- a/web/source/settings-panel/lib/api/index.js +++ b/web/source/settings-panel/lib/api/index.js @@ -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; } diff --git a/web/source/settings-panel/lib/api/oauth.js b/web/source/settings-panel/lib/api/oauth.js index 1b985e7bd..604bdcc37 100644 --- a/web/source/settings-panel/lib/api/oauth.js +++ b/web/source/settings-panel/lib/api/oauth.js @@ -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()); }; } diff --git a/web/source/settings-panel/lib/errors.js b/web/source/settings-panel/lib/errors.js index 6e4afefc0..c2f781cb2 100644 --- a/web/source/settings-panel/lib/errors.js +++ b/web/source/settings-panel/lib/errors.js @@ -23,4 +23,5 @@ const createError = require("create-error"); module.exports = { APIError: createError("APIError"), OAUTHError: createError("OAUTHError"), + AuthenticationError: createError("AuthenticationError"), }; \ No newline at end of file diff --git a/web/source/settings-panel/lib/generate-views.js b/web/source/settings-panel/lib/get-views.js similarity index 82% rename from web/source/settings-panel/lib/generate-views.js rename to web/source/settings-panel/lib/get-views.js index cfbbb641b..5c68e832c 100644 --- a/web/source/settings-panel/lib/generate-views.js +++ b/web/source/settings-panel/lib/get-views.js @@ -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( ); let childrenPath = `${base}/:section`; - panelRouter.push( + panelRouterEl.push( {routes} @@ -77,7 +96,7 @@ module.exports = function generateViews(struct) { ); - sidebar.push( + sidebarEl.push( diff --git a/web/source/settings-panel/redux/reducers/oauth.js b/web/source/settings-panel/redux/reducers/oauth.js index 2ff9af0fd..c332a7d06 100644 --- a/web/source/settings-panel/redux/reducers/oauth.js +++ b/web/source/settings-panel/redux/reducers/oauth.js @@ -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; } } }); \ No newline at end of file diff --git a/web/source/settings-panel/style.css b/web/source/settings-panel/style.css index ac6260c4e..507f09d04 100644 --- a/web/source/settings-panel/style.css +++ b/web/source/settings-panel/style.css @@ -51,6 +51,7 @@ section { border-bottom-right-radius: 0; display: flex; flex-direction: column; + min-width: 12rem; a { text-decoration: none; diff --git a/web/source/settings-panel/user/settings.js b/web/source/settings-panel/user/settings.js index 48a8ff241..86b1435e7 100644 --- a/web/source/settings-panel/user/settings.js +++ b/web/source/settings-panel/user/settings.js @@ -55,8 +55,9 @@ module.exports = function UserSettings() { return (

Post settings

- + }>