Merge pull request #212 from grant-project/redux-alien-auth-events
Handle External Auth Events & Fix Double Loading
This commit is contained in:
commit
84dd969136
|
@ -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
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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<any>(checkUser());
|
||||
}
|
||||
}
|
||||
lastAuthed = authed;
|
||||
return res;
|
||||
},
|
||||
// Try to parse error message if possible
|
||||
err => {
|
||||
if (err.response && err.response.data) {
|
||||
|
|
|
@ -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<any>) => {
|
||||
return async (dispatch: Dispatch<any>, 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();
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
@ -23,6 +23,11 @@ const bindMiddleware = (middleware: MiddleWare[]) => {
|
|||
return composeWithDevTools(applyMiddleware(...middleware));
|
||||
};
|
||||
|
||||
let storeRef = null as null | Store<AppState>;
|
||||
export function getStoreRef() {
|
||||
return storeRef;
|
||||
}
|
||||
|
||||
export function configureStore(initialState: Partial<AppState> = combineInitialState) {
|
||||
const store: Store<AppState> = createStore(
|
||||
rootReducer,
|
||||
|
@ -44,6 +49,6 @@ export function configureStore(initialState: Partial<AppState> = combineInitialS
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
storeRef = store;
|
||||
return { store };
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@ import { ChunkExtractor } from '@loadable/server';
|
|||
export interface Props {
|
||||
children: any;
|
||||
css: string[];
|
||||
scripts: string[];
|
||||
linkTags: Array<React.LinkHTMLAttributes<HTMLLinkElement>>;
|
||||
metaTags: Array<React.MetaHTMLAttributes<HTMLMetaElement>>;
|
||||
state: string;
|
||||
|
@ -15,7 +14,6 @@ export interface Props {
|
|||
|
||||
const HTML: React.SFC<Props> = ({
|
||||
children,
|
||||
scripts,
|
||||
css,
|
||||
state,
|
||||
i18n,
|
||||
|
@ -78,9 +76,6 @@ const HTML: React.SFC<Props> = ({
|
|||
</head>
|
||||
<body>
|
||||
<div id="app" dangerouslySetInnerHTML={{ __html: children }} />
|
||||
{scripts.map(src => {
|
||||
return <script key={src} src={src} />;
|
||||
})}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
|
|
@ -53,9 +53,6 @@ const serverRenderer = async (req: Request, res: Response) => {
|
|||
const cssFiles = ['bundle.css', 'vendor.css']
|
||||
.map(f => res.locals.assetPath(f))
|
||||
.filter(Boolean);
|
||||
const jsFiles = ['vendor.js', 'bundle.js']
|
||||
.map(f => res.locals.assetPath(f))
|
||||
.filter(Boolean);
|
||||
const mappedLinkTags = linkTags
|
||||
.map(l => ({ ...l, href: res.locals.assetPath(l.href) }))
|
||||
.filter(l => !!l.href);
|
||||
|
@ -66,7 +63,6 @@ const serverRenderer = async (req: Request, res: Response) => {
|
|||
const html = renderToString(
|
||||
<Html
|
||||
css={cssFiles}
|
||||
scripts={jsFiles}
|
||||
linkTags={mappedLinkTags}
|
||||
metaTags={mappedMetaTags}
|
||||
state={state}
|
||||
|
|
Loading…
Reference in New Issue