mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-28 20:53:24 +10:00
Merge pull request #49 from sharetribe/sdk-login-and-logout
E2E authentication with the SDK
This commit is contained in:
commit
14d0211e86
6 changed files with 100 additions and 8 deletions
|
|
@ -24,7 +24,7 @@
|
|||
"sharetribe-scripts": "0.8.6",
|
||||
"source-map-support": "^0.4.11",
|
||||
"url": "^0.11.0",
|
||||
"sharetribe-sdk": "git+ssh://git@github.com/sharetribe/sharetribe-sdk-js.git"
|
||||
"sharetribe-sdk": "git+ssh://git@github.com/sharetribe/sharetribe-sdk-js#eab535d1e489e262c72c9fb822a8aac36f22e514"
|
||||
},
|
||||
"devDependencies": {
|
||||
"enzyme": "^2.7.1",
|
||||
|
|
|
|||
|
|
@ -2,10 +2,16 @@
|
|||
/**
|
||||
* Authentication duck.
|
||||
*/
|
||||
import { call, put, take, cancel, fork } from 'redux-saga/effects';
|
||||
import { call, put, take, cancel, fork, takeLatest } from 'redux-saga/effects';
|
||||
|
||||
const authenticated = authInfo => authInfo.grantType === 'refresh_token';
|
||||
|
||||
// ================ Action types ================ //
|
||||
|
||||
export const AUTH_INFO_REQUEST = 'app/Auth/AUTH_INFO_REQUEST';
|
||||
export const AUTH_INFO_SUCCESS = 'app/Auth/AUTH_INFO_SUCCESS';
|
||||
export const AUTH_INFO_ERROR = 'app/Auth/AUTH_INFO_ERROR';
|
||||
|
||||
export const LOGIN_REQUEST = 'app/Auth/LOGIN_REQUEST';
|
||||
export const LOGIN_SUCCESS = 'app/Auth/LOGIN_SUCCESS';
|
||||
export const LOGIN_ERROR = 'app/Auth/LOGIN_ERROR';
|
||||
|
|
@ -16,17 +22,25 @@ export const LOGOUT_ERROR = 'app/Auth/LOGOUT_ERROR';
|
|||
|
||||
// ================ Reducer ================ //
|
||||
|
||||
const initialState = { isAuthenticated: false, error: null };
|
||||
const initialState = { authInfoLoaded: false, isAuthenticated: false, error: null };
|
||||
|
||||
export default function reducer(state = initialState, action = {}) {
|
||||
const { type, payload } = action;
|
||||
switch (type) {
|
||||
case AUTH_INFO_REQUEST:
|
||||
return { ...state, error: null };
|
||||
case AUTH_INFO_SUCCESS:
|
||||
return { ...state, authInfoLoaded: true, isAuthenticated: authenticated(payload) };
|
||||
case AUTH_INFO_ERROR:
|
||||
return { ...state, error: payload };
|
||||
|
||||
case LOGIN_REQUEST:
|
||||
return { ...state, error: null };
|
||||
case LOGIN_SUCCESS:
|
||||
return { ...state, isAuthenticated: true };
|
||||
case LOGIN_ERROR:
|
||||
return { ...state, error: payload };
|
||||
|
||||
case LOGOUT_REQUEST:
|
||||
return { ...state, error: null };
|
||||
case LOGOUT_SUCCESS:
|
||||
|
|
@ -40,6 +54,10 @@ export default function reducer(state = initialState, action = {}) {
|
|||
|
||||
// ================ Action creators ================ //
|
||||
|
||||
export const authInfo = () => ({ type: AUTH_INFO_REQUEST });
|
||||
export const authInfoSuccess = info => ({ type: AUTH_INFO_SUCCESS, payload: info });
|
||||
export const authInfoError = error => ({ type: AUTH_INFO_ERROR, payload: error, error: true });
|
||||
|
||||
export const login = (username, password) => ({
|
||||
type: LOGIN_REQUEST,
|
||||
payload: { username, password },
|
||||
|
|
@ -53,6 +71,15 @@ export const logoutError = error => ({ type: LOGOUT_ERROR, payload: error, error
|
|||
|
||||
// ================ Worker sagas ================ //
|
||||
|
||||
export function* callAuthInfo(sdk) {
|
||||
try {
|
||||
const info = yield call(sdk.authInfo);
|
||||
yield put(authInfoSuccess(info));
|
||||
} catch (e) {
|
||||
yield put(authInfoError(e));
|
||||
}
|
||||
}
|
||||
|
||||
export function* callLogin(action, sdk) {
|
||||
const { payload } = action;
|
||||
const { username, password } = payload;
|
||||
|
|
@ -78,6 +105,10 @@ export function* callLogout(action, sdk) {
|
|||
|
||||
// ================ Watcher sagas ================ //
|
||||
|
||||
export function* watchAuthInfo(sdk) {
|
||||
yield takeLatest(AUTH_INFO_REQUEST, callAuthInfo, sdk);
|
||||
}
|
||||
|
||||
export function* watchAuth(sdk) {
|
||||
let task;
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ import reducer, {
|
|||
NOTHING_IN_PROGRESS,
|
||||
LOGIN_IN_PROGRESS,
|
||||
LOGOUT_IN_PROGRESS,
|
||||
authInfo,
|
||||
authInfoSuccess,
|
||||
authInfoError,
|
||||
login,
|
||||
loginSuccess,
|
||||
loginError,
|
||||
|
|
@ -21,6 +24,7 @@ describe('Auth duck', () => {
|
|||
describe('reducer', () => {
|
||||
it('should be logged out by default', () => {
|
||||
const state = reducer();
|
||||
expect(state.authInfoLoaded).toEqual(false);
|
||||
expect(state.isAuthenticated).toEqual(false);
|
||||
expect(state.error).toBeNull();
|
||||
});
|
||||
|
|
@ -50,6 +54,36 @@ describe('Auth duck', () => {
|
|||
expect(state.isAuthenticated).toEqual(false);
|
||||
expect(state.error).toEqual(error);
|
||||
});
|
||||
|
||||
it('should set initial state for unauthenticated users', () => {
|
||||
const authInfoLoggedOut = {};
|
||||
const initialState = reducer();
|
||||
expect(initialState.authInfoLoaded).toEqual(false);
|
||||
const state = reducer(initialState, authInfoSuccess(authInfoLoggedOut));
|
||||
expect(state.authInfoLoaded).toEqual(true);
|
||||
expect(state.isAuthenticated).toEqual(false);
|
||||
expect(state.error).toBeNull();
|
||||
});
|
||||
|
||||
it('should set initial state for anonymous users', () => {
|
||||
const authInfoAnonymous = { grantType: 'client_credentials' };
|
||||
const initialState = reducer();
|
||||
expect(initialState.authInfoLoaded).toEqual(false);
|
||||
const state = reducer(initialState, authInfoSuccess(authInfoAnonymous));
|
||||
expect(state.authInfoLoaded).toEqual(true);
|
||||
expect(state.isAuthenticated).toEqual(false);
|
||||
expect(state.error).toBeNull();
|
||||
});
|
||||
|
||||
it('should set initial state for unauthenticated users', () => {
|
||||
const authInfoLoggedIn = { grantType: 'refresh_token' };
|
||||
const initialState = reducer();
|
||||
expect(initialState.authInfoLoaded).toEqual(false);
|
||||
const state = reducer(initialState, authInfoSuccess(authInfoLoggedIn));
|
||||
expect(state.authInfoLoaded).toEqual(true);
|
||||
expect(state.isAuthenticated).toEqual(true);
|
||||
expect(state.error).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('login worker', () => {
|
||||
|
|
|
|||
29
src/index.js
29
src/index.js
|
|
@ -21,6 +21,7 @@ import { matchPathname } from './util/routes';
|
|||
import * as sample from './util/sample';
|
||||
import createRootSaga from './sagas';
|
||||
import config from './config';
|
||||
import { authInfo } from './ducks/Auth.duck';
|
||||
import {
|
||||
showListings,
|
||||
queryListings,
|
||||
|
|
@ -31,6 +32,23 @@ import {
|
|||
|
||||
import './index.css';
|
||||
|
||||
const renderWithAuthInfo = store => {
|
||||
ReactDOM.render(<ClientApp store={store} />, document.getElementById('root'));
|
||||
};
|
||||
|
||||
// Wait for the store to have the Auth.authInfoLoaded flag, render the
|
||||
// application when the auth information is present.
|
||||
const renderWhenAuthInfoLoaded = store => {
|
||||
const unsubscribe = store.subscribe(() => {
|
||||
const authInfoLoaded = store.getState().Auth.authInfoLoaded;
|
||||
if (authInfoLoaded) {
|
||||
unsubscribe();
|
||||
renderWithAuthInfo(store);
|
||||
}
|
||||
});
|
||||
store.dispatch(authInfo());
|
||||
};
|
||||
|
||||
// If we're in a browser already, render the client application.
|
||||
if (typeof window !== 'undefined') {
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
|
|
@ -40,7 +58,16 @@ if (typeof window !== 'undefined') {
|
|||
|
||||
store.runSaga(createRootSaga(sdk));
|
||||
|
||||
ReactDOM.render(<ClientApp store={store} />, document.getElementById('root'));
|
||||
const authInfoLoaded = store.getState().Auth.authInfoLoaded;
|
||||
|
||||
// If the server already loaded the auth information, render the app
|
||||
// immediately. Otherwise wait for the flag to be loaded and render
|
||||
// when auth information is present.
|
||||
if (authInfoLoaded) {
|
||||
renderWithAuthInfo(store);
|
||||
} else {
|
||||
renderWhenAuthInfoLoaded(store);
|
||||
}
|
||||
|
||||
// Expose stuff for the browser REPL
|
||||
if (config.dev) {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { watchAuth } from './ducks/Auth.duck';
|
||||
import { watchAuthInfo, watchAuth } from './ducks/Auth.duck';
|
||||
import { watchLoadListings } from './containers/SearchPage/SearchPage.duck';
|
||||
import { watchSdk } from './ducks/sdk.duck';
|
||||
|
||||
const createRootSaga = sdk => function* rootSaga() {
|
||||
yield [watchAuth(sdk), watchLoadListings(sdk), watchSdk(sdk)];
|
||||
yield [watchAuthInfo(sdk), watchAuth(sdk), watchLoadListings(sdk), watchSdk(sdk)];
|
||||
};
|
||||
|
||||
export default createRootSaga;
|
||||
|
|
|
|||
|
|
@ -5675,9 +5675,9 @@ sharetribe-scripts@0.8.6:
|
|||
optionalDependencies:
|
||||
fsevents "1.0.14"
|
||||
|
||||
"sharetribe-sdk@git+ssh://git@github.com/sharetribe/sharetribe-sdk-js.git":
|
||||
"sharetribe-sdk@git+ssh://git@github.com/sharetribe/sharetribe-sdk-js#eab535d1e489e262c72c9fb822a8aac36f22e514":
|
||||
version "0.0.1"
|
||||
resolved "git+ssh://git@github.com/sharetribe/sharetribe-sdk-js.git#037a4328da4926428f6d36f19d349186d06f8aad"
|
||||
resolved "git+ssh://git@github.com/sharetribe/sharetribe-sdk-js#eab535d1e489e262c72c9fb822a8aac36f22e514"
|
||||
dependencies:
|
||||
axios "^0.15.3"
|
||||
js-cookie "^2.1.3"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue