From 003bccc9e878046bfd46e7fbca6273afb8265798 Mon Sep 17 00:00:00 2001 From: Jerko Steiner Date: Mon, 18 Mar 2019 13:13:52 +0500 Subject: [PATCH] projects/node: Add custom redirectTo after successful login --- packages/client/src/login/LoginActions.ts | 48 +++++++++++-------- packages/client/src/login/LoginConnector.tsx | 3 +- packages/client/src/login/LoginForm.test.tsx | 19 ++++++-- packages/client/src/login/LoginForm.tsx | 8 +++- packages/client/src/login/LoginReducer.ts | 16 ++++--- .../client/src/login/RegisterConnector.tsx | 4 +- .../client/src/login/RegisterForm.test.tsx | 31 +++++++++--- packages/client/src/login/RegisterForm.tsx | 8 +++- packages/client/src/redux/Connector.ts | 5 +- packages/client/src/renderer/isClientSide.ts | 3 +- packages/client/src/test-utils/TestUtils.tsx | 2 +- 11 files changed, 102 insertions(+), 45 deletions(-) diff --git a/packages/client/src/login/LoginActions.ts b/packages/client/src/login/LoginActions.ts index 7f5f68f..825d45b 100644 --- a/packages/client/src/login/LoginActions.ts +++ b/packages/client/src/login/LoginActions.ts @@ -3,58 +3,68 @@ import {IAPIDef, ICredentials, IUser} from '@rondo/common' import {IHTTPClient} from '../http/IHTTPClient' export enum LoginActionKeys { - USER_LOG_IN = 'USER_LOG_IN', - USER_LOG_IN_PENDING = 'USER_LOG_IN_PENDING', - USER_LOG_IN_REJECTED = 'USER_LOG_IN_REJECTED', + LOGIN = 'LOGIN', + LOGIN_PENDING = 'LOGIN_PENDING', + LOGIN_REJECTED = 'LOGIN_REJECTED', - USER_LOG_OUT = 'USER_LOG_OUT', - USER_LOG_OUT_PENDING = 'USER_LOG_OUT_PENDING', - USER_LOG_OUT_REJECTED = 'USER_LOG_OUT_REJECTED', + LOGIN_LOG_OUT = 'LOGIN_LOG_OUT', + LOGIN_LOG_OUT_PENDING = 'LOGIN_LOG_OUT_PENDING', + LOGIN_LOG_OUT_REJECTED = 'LOGIN_LOG_OUT_REJECTED', - REGISTER_USER = 'REGISTER_USER', - REGISTER_USER_PENDING = 'REGISTER_USER_PENDING', - REGISTER_USER_REJECTED = 'REGISTER_USER_REJECTED', + LOGIN_REGISTER = 'LOGIN_REGISTER', + LOGIN_REGISTER_PENDING = 'LOGIN_REGISTER_PENDING', + LOGIN_REGISTER_REJECTED = 'LOGIN_REGISTER_REJECTED', + + LOGIN_REDIRECT_SET = 'LOGIN_REDIRECT_SET', } export class LoginActions { constructor(protected readonly http: IHTTPClient) {} logIn = (credentials: ICredentials) - : IAction => { + : IAction => { return { payload: this.http.post('/auth/login', credentials), - type: LoginActionKeys.USER_LOG_IN, + type: LoginActionKeys.LOGIN, } } logInError = (error: Error) - : IErrorAction => { + : IErrorAction => { return { error, - type: LoginActionKeys.USER_LOG_IN_REJECTED, + type: LoginActionKeys.LOGIN_REJECTED, } } - logOut = (): IAction => { + logOut = (): IAction => { return { payload: this.http.get('/auth/logout'), - type: LoginActionKeys.USER_LOG_OUT, + type: LoginActionKeys.LOGIN_LOG_OUT, } } register = (profile: ICredentials): - IAction => { + IAction => { return { payload: this.http.post('/auth/register', profile), - type: LoginActionKeys.REGISTER_USER, + type: LoginActionKeys.LOGIN_REGISTER, } } registerError = (error: Error) - : IErrorAction => { + : IErrorAction => { return { error, - type: LoginActionKeys.REGISTER_USER_REJECTED, + type: LoginActionKeys.LOGIN_REGISTER_REJECTED, + } + } + + setRedirectTo = (redirectTo: string) + : IAction<{redirectTo: string}, LoginActionKeys.LOGIN_REDIRECT_SET> => { + return { + payload: {redirectTo}, + type: LoginActionKeys.LOGIN_REDIRECT_SET, } } } diff --git a/packages/client/src/login/LoginConnector.tsx b/packages/client/src/login/LoginConnector.tsx index d4554b6..6f6d404 100644 --- a/packages/client/src/login/LoginConnector.tsx +++ b/packages/client/src/login/LoginConnector.tsx @@ -12,7 +12,7 @@ const defaultCredentials: ICredentials = { password: '', } -export class LoginConnector extends Connector { +export class LoginConnector extends Connector { constructor(protected readonly loginActions: LoginActions) { super() @@ -24,6 +24,7 @@ export class LoginConnector extends Connector { state => ({ error: state.error, user: state.user, + redirectTo: state.redirectTo, }), dispatch => ({ onSubmit: bindActionCreators(this.loginActions.logIn, dispatch), diff --git a/packages/client/src/login/LoginForm.test.tsx b/packages/client/src/login/LoginForm.test.tsx index 1ebad58..738b33c 100644 --- a/packages/client/src/login/LoginForm.test.tsx +++ b/packages/client/src/login/LoginForm.test.tsx @@ -1,7 +1,8 @@ import * as Feature from './' +import ReactDOM from 'react-dom' +import T from 'react-dom/test-utils' import {HTTPClientMock, TestUtils, getError} from '../test-utils' import {IAPIDef} from '@rondo/common' -import T from 'react-dom/test-utils' const test = new TestUtils() @@ -10,15 +11,18 @@ describe('LoginForm', () => { const http = new HTTPClientMock() const loginActions = new Feature.LoginActions(http) - const t = test.withProvider({ + const createTestProvider = () => test.withProvider({ reducers: {Login: Feature.Login}, - state: {Login: {user: {id: 1}}}, connector: new Feature.LoginConnector(loginActions), select: state => state.Login, }) + beforeAll(() => { + (window as any).__MOCK_SERVER_SIDE__ = true + }) + it('should render', () => { - t.render() + createTestProvider().render() }) describe('submit', () => { @@ -26,6 +30,7 @@ describe('LoginForm', () => { const data = {username: 'user', password: 'pass'} const onSuccess = jest.fn() let node: Element + let component: React.Component beforeEach(() => { http.mockAdd({ method: 'post', @@ -33,8 +38,10 @@ describe('LoginForm', () => { data, }, {id: 123}) + const t = createTestProvider() const r = t.render({onSuccess}) node = r.node + component = r.component T.Simulate.change( node.querySelector('input[name="username"]')!, {target: {value: 'user'}} as any, @@ -45,7 +52,7 @@ describe('LoginForm', () => { ) }) - it('should submit a form and clear it', async () => { + it('should submit a form, clear it and redirect it', async () => { T.Simulate.submit(node) const {req} = await http.wait() expect(req).toEqual({ @@ -64,6 +71,8 @@ describe('LoginForm', () => { .value, ) .toEqual('') + node = ReactDOM.findDOMNode(component) as Element + expect(node.innerHTML).toMatch(//) }) it('sets the error message on failure', async () => { diff --git a/packages/client/src/login/LoginForm.tsx b/packages/client/src/login/LoginForm.tsx index 99466f7..5695186 100644 --- a/packages/client/src/login/LoginForm.tsx +++ b/packages/client/src/login/LoginForm.tsx @@ -1,18 +1,24 @@ import React from 'react' -import {ICredentials} from '@rondo/common' +import {ICredentials, IUser} from '@rondo/common' import {Input} from '../components/Input' +import {Redirect} from '../components/Redirect' export interface ILoginFormProps { error?: string onSubmit: () => void onChange: (name: string, value: string) => void data: ICredentials + user?: IUser + redirectTo: string } // TODO maybe replace this with Formik, which is recommended in React docs // https://jaredpalmer.com/formik/docs/overview export class LoginForm extends React.PureComponent { render() { + if (this.props.user) { + return + } return (

{this.props.error}

diff --git a/packages/client/src/login/LoginReducer.ts b/packages/client/src/login/LoginReducer.ts index d92ebb7..db62a3c 100644 --- a/packages/client/src/login/LoginReducer.ts +++ b/packages/client/src/login/LoginReducer.ts @@ -2,13 +2,15 @@ import {IUser} from '@rondo/common' import {LoginActionKeys, LoginActionType} from './LoginActions' export interface ILoginState { - readonly error?: string, + readonly error?: string readonly user?: IUser + readonly redirectTo: string } const defaultState: ILoginState = { error: undefined, user: undefined, + redirectTo: '/', } export function Login( @@ -16,16 +18,18 @@ export function Login( action: LoginActionType, ): ILoginState { switch (action.type) { - case LoginActionKeys.USER_LOG_IN: + case LoginActionKeys.LOGIN: return {...state, user: action.payload, error: ''} - case LoginActionKeys.USER_LOG_OUT: + case LoginActionKeys.LOGIN_LOG_OUT: return {...state, user: undefined} - case LoginActionKeys.USER_LOG_IN_REJECTED: + case LoginActionKeys.LOGIN_REJECTED: return {...state, error: action.error.message} - case LoginActionKeys.REGISTER_USER: + case LoginActionKeys.LOGIN_REGISTER: return {...state, user: action.payload, error: ''} - case LoginActionKeys.REGISTER_USER_REJECTED: + case LoginActionKeys.LOGIN_REGISTER_REJECTED: return {...state, error: action.error.message} + case LoginActionKeys.LOGIN_REDIRECT_SET: + return {...state, redirectTo: action.payload.redirectTo} default: return state } diff --git a/packages/client/src/login/RegisterConnector.tsx b/packages/client/src/login/RegisterConnector.tsx index 8b34517..730b5d4 100644 --- a/packages/client/src/login/RegisterConnector.tsx +++ b/packages/client/src/login/RegisterConnector.tsx @@ -12,7 +12,7 @@ const defaultCredentials: ICredentials = { password: '', } -export class RegisterConnector extends Connector { +export class RegisterConnector extends Connector { constructor(protected readonly loginActions: LoginActions) { super() @@ -23,6 +23,8 @@ export class RegisterConnector extends Connector { getLocalState, state => ({ error: state.error, + user: state.user, + redirectTo: state.redirectTo, }), dispatch => ({ onSubmit: bindActionCreators(this.loginActions.register, dispatch), diff --git a/packages/client/src/login/RegisterForm.test.tsx b/packages/client/src/login/RegisterForm.test.tsx index 582f91d..c2d03b5 100644 --- a/packages/client/src/login/RegisterForm.test.tsx +++ b/packages/client/src/login/RegisterForm.test.tsx @@ -1,7 +1,8 @@ import * as Feature from './' +import ReactDOM from 'react-dom' +import T from 'react-dom/test-utils' import {HTTPClientMock, TestUtils, getError} from '../test-utils' import {IAPIDef} from '@rondo/common' -import T from 'react-dom/test-utils' const test = new TestUtils() @@ -10,15 +11,18 @@ describe('RegisterForm', () => { const http = new HTTPClientMock() const loginActions = new Feature.LoginActions(http) - const t = test.withProvider({ + const createTestProvider = () => test.withProvider({ reducers: {Login: Feature.Login}, - state: {Login: {user: {id: 1}}}, connector: new Feature.RegisterConnector(loginActions), select: state => state.Login, }) + beforeAll(() => { + (window as any).__MOCK_SERVER_SIDE__ = true + }) + it('should render', () => { - t.render() + createTestProvider().render() }) describe('submit', () => { @@ -26,6 +30,7 @@ describe('RegisterForm', () => { const data = {username: 'user', password: 'pass'} const onSuccess = jest.fn() let node: Element + let component: React.Component beforeEach(() => { http.mockAdd({ method: 'post', @@ -33,7 +38,9 @@ describe('RegisterForm', () => { data, }, {id: 123}) - node = t.render({onSuccess}).node + const r = createTestProvider().render({onSuccess}) + node = r.node + component = r.component T.Simulate.change( node.querySelector('input[name="username"]')!, {target: {value: 'user'}} as any, @@ -44,7 +51,7 @@ describe('RegisterForm', () => { ) }) - it('should submit a form', async () => { + it('should submit a form, clear it, and redirect', async () => { T.Simulate.submit(node) const {req} = await http.wait() expect(req).toEqual({ @@ -53,6 +60,18 @@ describe('RegisterForm', () => { data, }) expect(onSuccess.mock.calls.length).toBe(1) + expect( + (node.querySelector('input[name="username"]') as HTMLInputElement) + .value, + ) + .toEqual('') + expect( + (node.querySelector('input[name="password"]') as HTMLInputElement) + .value, + ) + .toEqual('') + node = ReactDOM.findDOMNode(component) as Element + expect(node.innerHTML).toMatch(/
/) }) it('sets the error message on failure', async () => { diff --git a/packages/client/src/login/RegisterForm.tsx b/packages/client/src/login/RegisterForm.tsx index 769aa14..92fa075 100644 --- a/packages/client/src/login/RegisterForm.tsx +++ b/packages/client/src/login/RegisterForm.tsx @@ -1,16 +1,22 @@ import React from 'react' -import {ICredentials} from '@rondo/common' +import {ICredentials, IUser} from '@rondo/common' import {Input} from '../components/Input' +import {Redirect} from '../components/Redirect' export interface IRegisterFormProps { error?: string onSubmit: () => void onChange: (name: string, value: string) => void data: ICredentials + user?: IUser + redirectTo: string } export class RegisterForm extends React.PureComponent { render() { + if (this.props.user) { + return + } return (

{this.props.error}

diff --git a/packages/client/src/redux/Connector.ts b/packages/client/src/redux/Connector.ts index ccc97b0..2775a77 100644 --- a/packages/client/src/redux/Connector.ts +++ b/packages/client/src/redux/Connector.ts @@ -18,7 +18,7 @@ import {ComponentType} from 'react' * easy to mock it during tests, or swap out different dependencies for * different applications. */ -export abstract class Connector { +export abstract class Connector { /** * Connects a component using redux. The `selectState` method is used to @@ -29,13 +29,12 @@ export abstract class Connector { * * https://stackoverflow.com/questions/54277411 */ - abstract connect( + abstract connect( selectState: IStateSelector, ): ComponentType protected wrap< State, - LocalState, StateProps, DispatchProps, Props diff --git a/packages/client/src/renderer/isClientSide.ts b/packages/client/src/renderer/isClientSide.ts index d907866..ad83dcb 100644 --- a/packages/client/src/renderer/isClientSide.ts +++ b/packages/client/src/renderer/isClientSide.ts @@ -1,5 +1,6 @@ export function isClientSide() { return typeof window !== 'undefined' && typeof window.document !== 'undefined' && - typeof window.document.createElement === 'function' + typeof window.document.createElement === 'function' && + typeof (window as any).__MOCK_SERVER_SIDE__ === 'undefined' } diff --git a/packages/client/src/test-utils/TestUtils.tsx b/packages/client/src/test-utils/TestUtils.tsx index 368951e..d44dab5 100644 --- a/packages/client/src/test-utils/TestUtils.tsx +++ b/packages/client/src/test-utils/TestUtils.tsx @@ -16,7 +16,7 @@ import { interface IRenderParams { reducers: ReducersMapObject state?: DeepPartial - connector: Connector + connector: Connector select: IStateSelector }