diff --git a/backend/grant/app.py b/backend/grant/app.py index 73a17bcd..7f8632c6 100644 --- a/backend/grant/app.py +++ b/backend/grant/app.py @@ -9,7 +9,7 @@ from grant import commands, proposal, user, comment, milestone, admin, email, bl from grant.extensions import bcrypt, migrate, db, ma, security from grant.settings import SENTRY_RELEASE, ENV from sentry_sdk.integrations.flask import FlaskIntegration -from grant.utils.auth import AuthException, handle_auth_error +from grant.utils.auth import AuthException, handle_auth_error, get_authed_user def create_app(config_objects=["grant.settings"]): @@ -33,6 +33,11 @@ def create_app(config_objects=["grant.settings"]): # NOTE: testing mode does not honor this handler, and instead returns the generic 500 response app.register_error_handler(AuthException, handle_auth_error) + @app.after_request + def grantio_authed(response): + response.headers["X-Grantio-Authed"] = 'yes' if get_authed_user() else 'no' + return response + return app @@ -46,7 +51,7 @@ def register_extensions(app): security.init_app(app, datastore=user_datastore, register_blueprint=False) # supports_credentials for session cookies - CORS(app, supports_credentials=True) + CORS(app, supports_credentials=True, expose_headers='X-Grantio-Authed') SSLify(app) return None diff --git a/backend/grant/utils/auth.py b/backend/grant/utils/auth.py index 112bde74..c5722989 100644 --- a/backend/grant/utils/auth.py +++ b/backend/grant/utils/auth.py @@ -18,7 +18,7 @@ def handle_auth_error(e): def get_authed_user(): - return current_user if current_user.is_authenticated else None + return current_user if current_user.is_authenticated and not current_user.banned else None def throw_on_banned(user): diff --git a/frontend/client/api/axios.ts b/frontend/client/api/axios.ts index b9bd2939..370c39b0 100644 --- a/frontend/client/api/axios.ts +++ b/frontend/client/api/axios.ts @@ -1,4 +1,6 @@ import axios from 'axios'; +import { getStoreRef } from 'store/configure'; +import { checkUser } from 'modules/auth/actions'; const instance = axios.create({ baseURL: process.env.BACKEND_URL, @@ -7,9 +9,24 @@ const instance = axios.create({ withCredentials: true, }); +let lastAuthed = null as null | string; + instance.interceptors.response.use( - // Do nothing to responses - res => res, + // - watch for changes to auth header and trigger checkUser action if it changes + // - this allows for external authorization events to be registered in this context + // - external auth events include login/logout in another tab, or + // the user getting banned + res => { + const authed = res.headers['x-grantio-authed']; + if (lastAuthed !== null && lastAuthed !== authed) { + const store = getStoreRef(); + if (store) { + store.dispatch(checkUser()); + } + } + lastAuthed = authed; + return res; + }, // Try to parse error message if possible err => { if (err.response && err.response.data) { diff --git a/frontend/client/modules/auth/actions.ts b/frontend/client/modules/auth/actions.ts index cf0c2285..7258d5c4 100644 --- a/frontend/client/modules/auth/actions.ts +++ b/frontend/client/modules/auth/actions.ts @@ -8,8 +8,11 @@ import { authUser as apiAuthUser, logoutUser, } from 'api/api'; +import { AppState } from 'store/reducers'; import { User } from 'types'; +type GetState = () => AppState; + function setSentryScope(user: User) { Sentry.configureScope(scope => { scope.setUser({ @@ -20,7 +23,17 @@ function setSentryScope(user: User) { // check if user has authenticated session export function checkUser() { - return async (dispatch: Dispatch) => { + return async (dispatch: Dispatch, getState: GetState) => { + const state = getState(); + if (state.auth.isAuthingUser || state.auth.isLoggingOut) { + // this happens when axios calls checkUser upon seeing a change in the + // custom auth-header, this call will not be ignored on other tabs not + // initiating the authentication related behaviors + console.info( + 'ignoring checkUser action b/c we are currently authing or logging out', + ); + return; + } dispatch({ type: types.CHECK_USER_PENDING }); try { const res = await checkUserAuth(); diff --git a/frontend/client/modules/auth/reducers.ts b/frontend/client/modules/auth/reducers.ts index 66fdff5c..f0afd677 100644 --- a/frontend/client/modules/auth/reducers.ts +++ b/frontend/client/modules/auth/reducers.ts @@ -13,6 +13,8 @@ export interface AuthState { isCheckingUser: boolean; hasCheckedUser: boolean; + isLoggingOut: boolean; + isCreatingUser: boolean; createUserError: string | null; @@ -35,6 +37,8 @@ export const INITIAL_STATE: AuthState = { isCheckingUser: false, hasCheckedUser: false, + isLoggingOut: false, + authSignature: null, authSignatureAddress: null, isSigningAuth: false, @@ -81,6 +85,7 @@ export default function createReducer( case types.CHECK_USER_REJECTED: return { ...state, + user: null, isCheckingUser: false, hasCheckedUser: true, }; @@ -135,9 +140,22 @@ export default function createReducer( signAuthError: action.payload, }; + case types.LOGOUT_PENDING: + return { + ...state, + isLoggingOut: true, + user: null, + }; case types.LOGOUT_FULFILLED: return { ...state, + isLoggingOut: false, + user: null, + }; + case types.LOGOUT_REJECTED: + return { + ...state, + isLoggingOut: false, user: null, }; diff --git a/frontend/client/store/configure.tsx b/frontend/client/store/configure.tsx index 75fe415d..3fe92584 100644 --- a/frontend/client/store/configure.tsx +++ b/frontend/client/store/configure.tsx @@ -23,6 +23,11 @@ const bindMiddleware = (middleware: MiddleWare[]) => { return composeWithDevTools(applyMiddleware(...middleware)); }; +let storeRef = null as null | Store; +export function getStoreRef() { + return storeRef; +} + export function configureStore(initialState: Partial = combineInitialState) { const store: Store = createStore( rootReducer, @@ -44,6 +49,6 @@ export function configureStore(initialState: Partial = combineInitialS ); } } - + storeRef = store; return { store }; } diff --git a/frontend/server/components/HTML.tsx b/frontend/server/components/HTML.tsx index 5c976eb8..416c9383 100644 --- a/frontend/server/components/HTML.tsx +++ b/frontend/server/components/HTML.tsx @@ -5,7 +5,6 @@ import { ChunkExtractor } from '@loadable/server'; export interface Props { children: any; css: string[]; - scripts: string[]; linkTags: Array>; metaTags: Array>; state: string; @@ -15,7 +14,6 @@ export interface Props { const HTML: React.SFC = ({ children, - scripts, css, state, i18n, @@ -78,9 +76,6 @@ const HTML: React.SFC = ({
- {scripts.map(src => { - return