From 5e44c4d5d55b17a83b40664a556aee88166b83cc Mon Sep 17 00:00:00 2001 From: Ivan Isakov Date: Wed, 10 Apr 2019 15:30:47 +0300 Subject: [PATCH 001/104] Change versions of welcom module --- packages/client/package.json | 2 +- packages/server/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/client/package.json b/packages/client/package.json index e3b2afa..d69a9ac 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -47,7 +47,7 @@ "@restapp/i18n-common-react": "^0.1.0", "@restapp/module-client-react": "^0.1.0", "@restapp/validation-common-react": "^0.1.0", - "@restapp/welcome-client-react": "^1.0.0", + "@restapp/welcome-client-react": "^0.1.0", "antd": "^3.10.0", "babel-polyfill": "^6.26.0", "bootstrap": "^4.1.1", diff --git a/packages/server/package.json b/packages/server/package.json index 5ad2263..8682c08 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -54,7 +54,7 @@ "@restapp/core-common": "^0.1.0", "@restapp/database-server-ts": "^0.1.0", "@restapp/testing-server-ts": "^0.1.0", - "@restapp/welcome-server-ts": "^1.0.0", + "@restapp/welcome-server-ts": "^0.1.0", "@types/pdfmake": "^0.1.3", "bcryptjs": "^2.4.3", "body-parser": "^1.18.3", From f020440e8ea616afd27689236a2c8fc319a0868e Mon Sep 17 00:00:00 2001 From: Ivan Isakov Date: Tue, 7 May 2019 08:33:28 +0300 Subject: [PATCH 002/104] Create request middleware --- modules/core/common/createReduxStore.ts | 37 +++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/modules/core/common/createReduxStore.ts b/modules/core/common/createReduxStore.ts index de0181c..63a7586 100644 --- a/modules/core/common/createReduxStore.ts +++ b/modules/core/common/createReduxStore.ts @@ -8,11 +8,44 @@ export const getStoreReducer = (reducers: any) => ...reducers }); +const requestMiddleware: Middleware = _state => next => action => { + const { type, request, ...rest } = action; + if (!request) { + return next(action); + } + + if (!type) { + request(); + return next(action); + } + + const [REQUEST, SUCCESS, FAIL] = type; + next({ type: REQUEST, ...rest }); + + (async () => { + try { + const { data } = await request(); + next({ + type: SUCCESS, + payload: data, + ...rest + }); + } catch (error) { + next({ + type: FAIL, + payload: error, + ...rest + }); + } + })(); +}; + const createReduxStore = (reducers: Reducer, initialState: DeepPartial, routerMiddleware?: Middleware): Store => { + const middleware = routerMiddleware ? [routerMiddleware, requestMiddleware] : [requestMiddleware]; return createStore( getStoreReducer(reducers), - initialState, // initial state - routerMiddleware ? composeWithDevTools(applyMiddleware(routerMiddleware)) : undefined + initialState, // initial state, + composeWithDevTools(applyMiddleware(...middleware)) ); }; From fcba3ef2bd014ae15baeca58e88f17d97e6c93f5 Mon Sep 17 00:00:00 2001 From: Ivan Isakov Date: Tue, 7 May 2019 08:40:05 +0300 Subject: [PATCH 003/104] Add locales --- .../client-react/locales/en/translations.json | 193 ++++++++++++++++++ modules/user/client-react/locales/index.ts | 5 + .../client-react/locales/ru/translations.json | 191 +++++++++++++++++ modules/user/client-react/package.json | 6 + 4 files changed, 395 insertions(+) create mode 100644 modules/user/client-react/locales/en/translations.json create mode 100644 modules/user/client-react/locales/index.ts create mode 100644 modules/user/client-react/locales/ru/translations.json create mode 100644 modules/user/client-react/package.json diff --git a/modules/user/client-react/locales/en/translations.json b/modules/user/client-react/locales/en/translations.json new file mode 100644 index 0000000..76234b7 --- /dev/null +++ b/modules/user/client-react/locales/en/translations.json @@ -0,0 +1,193 @@ +{ + "loading": "App is loading...", + "navLink": { + "users": "Users", + "signIn": "Sign In", + "logout": "Log out", + "profile": "Profile", + "editProfile": "Edit profile", + "editUser": "Edit user", + "forgotPassword": "Forgot password", + "register": "Register", + "resetPassword": "Reset password" + }, + "forgotPass": { + "title": "Forgot password", + "meta": "A Forgot Password page example", + "errorMsg": "Reset password failed!", + "form": { + "title": "Password Reset", + "submitMsg": "Instructions for resetting your password were emailed to you.", + "fldEmail": "Email", + "btnSubmit": "Send instructions" + } + }, + + "login": { + "title": "Login", + "meta": "A Login page example", + "errorMsg": "Login failed!", + "cardTitle": "Available logins", + "notRegText": "Not registered yet?", + "fbBtn": "Log in with Facebook", + "googleBtn": "Log in with Google", + "githubBtn": "Log in with GitHub", + "linkedinBtn": "Log in with LinkedIn", + "form": { + "title": "Sign In", + "field": { + "usernameOrEmail": "Username or Email", + "pass": "Password" + }, + "btnSubmit": "Login" + }, + "btn": { + "forgotPass": "Forgot your password?", + "notReg": "Not registered yet?", + "sign": "Sign Up" + } + }, + + "profile": { + "title": "Profile", + "headerText": "Profile info", + "meta": "A Profile page example", + "loadMsg": "Loading...", + "errorMsg": "No current user is logged in", + "editProfileText": "Edit Profile", + "card": { + "title": "Profile", + "group": { + "name": "User Name", + "email": "Email", + "role": "Role", + "full": "Full Name" + }, + "fldEmail": "Email", + "btnSubmit": "Send instructions" + } + }, + + "reg": { + "title": "Register", + "meta": "A Registration page example", + "errorMsg": "Registration failed!", + "form": { + "title": "Sign Up", + "field": { + "name": "Username", + "email": "Email", + "pass": "Password", + "passConf": "Password Confirmation" + }, + "btnSubmit": "Register" + }, + "confirmationMsgTitle": "Verify Your Email Address", + "confirmationMsgBody": "We now need to verify your email address. We've sent you the email. Please click the link in that email to continue. ", + "confirmationBtn": "Go back to Login page", + "successRegTitle": "Email Has Been Verified Successfully", + "successRegBody": "Your email has been verified successfully. You can sign in using your login and password." + }, + + "resetPass": { + "title": "Reset password", + "meta": "A Reset Password page example", + "errorMsg": "Password reset failed!", + "form": { + "title": "Reset Password", + "field": { + "pass": "Password", + "passConf": "Password Confirmation" + }, + "btnSubmit": "Reset password" + } + }, + + "userEdit": { + "title": "Edit User", + "meta": "An Edit User page example", + "errorMsg": "User editing failed!", + "loadMsg": "Loading...", + "form": { + "title": "User", + "titleEdit": "Edit", + "titleCreate": "Create", + "field": { + "name": "Username", + "email": "Email", + "role": { + "label": "Role", + "user": "user", + "admin": "admin" + }, + "active": "Is Active", + "firstName": "First Name", + "lastName": "Last Name", + "pass": "Password", + "passConf": "Password Confirmation" + }, + "btnSubmit": "Save" + }, + "select": { + "okText": "Done", + "dismissText": "Cancel" + }, + "btnBack": "Back" + }, + "userAdd": { + "errorMsg": "Add user failed!" + }, + + "users": { + "title": "Users", + "meta": "A User List page example", + "loadMsg": "Loading...", + "list": { + "title": "Users", + "item": { + "filter": "Filter", + "search": "Search ...", + "role": { + "label": "Role", + "all": "All", + "user": "user", + "admin": "admin" + }, + "active": "Is Active" + } + }, + "select": { + "okText": "Done", + "dismissText": "Cancel" + }, + "btnModalSubmit": "Submit", + "btnModalClose": "Close", + "orderByText": "Order by", + "column": { + "name": "Username", + "email": "Email", + "role": "Role", + "active": "Is Active", + "actions": "Actions" + }, + "btn": { + "add": "Add user", + "delete": "Delete" + } + }, + + "nativeMock": "Hello, ", + "mobile": { + "login": { + "usernameOrEmail": { + "label": "Username or Email", + "placeholder": "Enter Your Username or Email" + }, + "pass": { + "label": "Password", + "placeholder": "Enter Your Password" + } + }, + "logout": "Log out" + } +} \ No newline at end of file diff --git a/modules/user/client-react/locales/index.ts b/modules/user/client-react/locales/index.ts new file mode 100644 index 0000000..d9fb121 --- /dev/null +++ b/modules/user/client-react/locales/index.ts @@ -0,0 +1,5 @@ +/* + * The index.js can be empty, it's just needed to point the loader to the root directory of the locales. + * https://github.com/alienfast/i18next-loader#option-2-use-with-import-syntax + */ +export default {}; diff --git a/modules/user/client-react/locales/ru/translations.json b/modules/user/client-react/locales/ru/translations.json new file mode 100644 index 0000000..80f778d --- /dev/null +++ b/modules/user/client-react/locales/ru/translations.json @@ -0,0 +1,191 @@ +{ + "loading": "Приложение загружается...", + "navLink": { + "users": "Пользователи", + "sign": "Вход в систему", + "logout": "Выход", + "profile": "Профиль", + "editProfile": "Редактирование профиля", + "editUser": "Редактирование пользователя", + "forgotPassword": "Забыли пароль", + "register": "Регистрация", + "resetPassword": "Сброс пароля" + }, + "forgotPass": { + "title": "Забыли пароль", + "meta": "Пример страницы Забыли пароль?", + "errorMsg": "Сброс пароля не выполнен!", + "form": { + "title": "Сброс пароля", + "submitMsg": "Инструкции по сбросу пароля были отправлены вам на электронную почту.", + "fldEmail": "Электронная почта", + "btnSubmit": "Отправить инструкции" + } + }, + + "login": { + "title": "Вход", + "meta": "Пример страницы с входом", + "errorMsg": "Ошибка входа!", + "cardTitle": "Доступные учетные записи", + "notRegText": "Ещё не зарегистрированы?", + "fbBtn": "Войти через Facebook", + "googleBtn": "Войти через Google", + "githubBtn": "Войти через GitHub", + "linkedinBtn": "Войти через LinkedIn", + "form": { + "title": "Вход в систему", + "field": { + "usernameOrEmail": "Имя пользователя или эл.почта", + "pass": "Пароль" + }, + "btnSubmit": "Войти" + }, + "btn": { + "forgotPass": "Забыли пароль?", + "notReg": "Ещё не зарегистрированы?", + "sign": "Зарегистрироваться" + } + }, + + "profile": { + "title": "Профиль", + "headerText": "Информация о профиле", + "meta": "Пример страницы с профилем", + "loadMsg": "Загрузка...", + "errorMsg": "Нет текущего пользователя", + "editProfileText": "Редактировать профиль", + "card": { + "title": "Профиль", + "group": { + "name": "Имя пользователя", + "email": "Электронная почта", + "role": "Роль", + "full": "Полное имя" + } + } + }, + + "reg": { + "title": "Регистрация", + "meta": "Пример страницы с регистрацией", + "errorMsg": "Ошибка регистрации!", + "form": { + "title": "Регистрация", + "field": { + "name": "Имя пользователя", + "email": "Электронная почта", + "pass": "Пароль", + "passConf": "Подтверждение пароля" + }, + "btnSubmit": "Зарегистрироваться" + }, + "confirmationMsgTitle": "Подтвердите свой адрес электронной почты", + "confirmationMsgBody": "Теперь нам нужно подтвердить ваш адрес электронной почты. Мы отправили вам электронное письмо. Пожалуйста, нажмите на ссылку в этом письме, чтобы продолжить.", + "confirmationBtn": "Вернуться на страница регистрации", + "successRegTitle": "Адрес электронной почты был успешно подтвержден", + "successRegBody": "Ваш адрес электронной почты был успешно подтвержден. Вы можете войти в приложение, используя свой логин и пароль." + }, + + "resetPass": { + "title": "Сброс пароля", + "meta": "Пример страницы со сбросом пароля", + "errorMsg": "Сброс пароля не выполнен!", + "form": { + "title": "Сброс пароля", + "field": { + "pass": "Пароля", + "passConf": "Подтверждение пароля" + }, + "btnSubmit": "Сбросить пароль" + } + }, + + "userEdit": { + "title": "Редактирование пользователя", + "meta": "Пример страницы с редактированием пользователя", + "errorMsg": "Не удалось изменить пользователя!", + "loadMsg": "Загрузка...", + "form": { + "title": "пользователя", + "titleEdit": "Редактировать", + "titleCreate": "Создать", + "field": { + "name": "Имя пользователя", + "email": "Электронная почта", + "role": { + "label": "Роль", + "user": "пользователь", + "admin": "админ" + }, + "active": "Активен", + "firstName": "Имя", + "lastName": "Фамилия", + "pass": "Пароль", + "passConf": "Подтверждение пароля" + }, + "btnSubmit": "Сохранить" + }, + "select": { + "okText": "Подтвердить", + "dismissText": "Отмена" + }, + "btnBack": "Назад" + }, + "userAdd": { + "errorMsg": "Не удалось добавить пользователя!" + }, + + "users": { + "title": "Список пользователей", + "meta": "Пример страницы со списком пользователей", + "loadMsg": "Загрузка...", + "list": { + "title": "Список пользователей", + "item": { + "filter": "Фильтр", + "search": "Поиск ...", + "role": { + "label": "Роль", + "all": "Все", + "user": "пользователь", + "admin": "админ" + }, + "active": "Активен" + } + }, + "select": { + "okText": "Подтвердить", + "dismissText": "Отмена" + }, + "btnModalSubmit": "Сохранить", + "btnModalClose": "Закрыть", + "orderByText": "Сортировать", + "column": { + "name": "Имя пользователя", + "email": "Электронная почта", + "role": "Роль", + "active": "Активен", + "actions": "Действия" + }, + "btn": { + "add": "Добавить пользователя", + "delete": "Удалить" + } + }, + + "nativeMock": "Привет, ", + "mobile": { + "login": { + "usernameOrEmail": { + "label": "Имя пользователя или email", + "placeholder": "Введите имя пользователя или эл.почту" + }, + "pass": { + "label": "Пароль", + "placeholder": "Введите пароль" + } + }, + "logout": "Выйти" + } +} \ No newline at end of file diff --git a/modules/user/client-react/package.json b/modules/user/client-react/package.json new file mode 100644 index 0000000..526f145 --- /dev/null +++ b/modules/user/client-react/package.json @@ -0,0 +1,6 @@ + +{ + "name": "@restapp/user-client-react", + "version": "0.1.0", + "private": true +} \ No newline at end of file From e65edd9bb83704489bdd8cee5a4fdc0409e4bc88 Mon Sep 17 00:00:00 2001 From: Ivan Isakov Date: Tue, 7 May 2019 08:55:39 +0300 Subject: [PATCH 004/104] Add auth base components --- .../client-react/containers/Auth.native.tsx | 1 + modules/user/client-react/containers/Auth.tsx | 41 ++++++++++++ .../user/client-react/containers/AuthBase.tsx | 67 +++++++++++++++++++ 3 files changed, 109 insertions(+) create mode 100644 modules/user/client-react/containers/Auth.native.tsx create mode 100644 modules/user/client-react/containers/Auth.tsx create mode 100644 modules/user/client-react/containers/AuthBase.tsx diff --git a/modules/user/client-react/containers/Auth.native.tsx b/modules/user/client-react/containers/Auth.native.tsx new file mode 100644 index 0000000..b8bad20 --- /dev/null +++ b/modules/user/client-react/containers/Auth.native.tsx @@ -0,0 +1 @@ +export * from './AuthBase'; diff --git a/modules/user/client-react/containers/Auth.tsx b/modules/user/client-react/containers/Auth.tsx new file mode 100644 index 0000000..08f7122 --- /dev/null +++ b/modules/user/client-react/containers/Auth.tsx @@ -0,0 +1,41 @@ +import * as React from 'react'; +import { Route, Redirect, RouteComponentProps } from 'react-router-dom'; +import { withLoadedUser } from './AuthBase'; +import { UserRole, User } from '..'; +import { WithUserProps } from './AuthBase'; + +interface AuthRouteProps extends WithUserProps { + role?: UserRole | UserRole[]; + redirect?: string; + redirectOnLoggedIn?: boolean; + component?: React.ComponentType> | React.ComponentType; +} + +const AuthRoute: React.ComponentType = withLoadedUser( + ({ currentUser, role, redirect = '/register', redirectOnLoggedIn, component: Component, ...rest }) => { + const RenderComponent: React.FunctionComponent = props => { + // The users is not logged in + if (redirectOnLoggedIn && currentUser) { + return ; + } + + return isRoleMatch(role, currentUser) ? ( + + ) : ( + + ); + }; + + return ; + } +); + +const isRoleMatch = (role: UserRole | UserRole[], currentUser: User) => { + if (!role) { + return true; + } + return currentUser && (Array.isArray(role) ? role : [role]).includes(currentUser.role); +}; + +export * from './AuthBase'; +export { AuthRoute }; diff --git a/modules/user/client-react/containers/AuthBase.tsx b/modules/user/client-react/containers/AuthBase.tsx new file mode 100644 index 0000000..cf6f1d1 --- /dev/null +++ b/modules/user/client-react/containers/AuthBase.tsx @@ -0,0 +1,67 @@ +import * as React from 'react'; +import { RouteProps } from 'react-router'; +import { History } from 'history'; +import { connect } from 'react-redux'; +import { CURRENT_USER } from '../actions'; +import { User, UserRole } from '..'; +import { ActionType } from '../reducers'; + +export interface WithUserProps extends RouteProps { + currentUser?: User; + currentUserLoading?: boolean; + refetchCurrentUser?: any; + children?: Element | any; + getCurrentUser?: () => void; +} + +interface IfLoggedInComponent { + role?: UserRole | UserRole[]; + currentUser?: User; + refetchCurrentUser?: any; + elseComponent?: Element | any; + children?: Element | any; +} + +export interface WithLogoutProps extends WithUserProps { + logout?: () => void; + history?: History; + clearUser?: () => void; +} + +const withUser = (Component: React.ComponentType) => { + const WithUser = ({ currentUser, ...rest }: WithUserProps) => { + return ; + }; + return connect(({ user: { loading, currentUser } }: any) => ({ + currentUserLoading: loading, + currentUser + }))(WithUser); +}; + +const hasRole = (role: UserRole | UserRole[], currentUser: User) => { + return currentUser && (!role || (Array.isArray(role) ? role : [role]).indexOf(currentUser.role) >= 0) ? true : false; +}; + +const withLoadedUser = (Component: React.ComponentType) => { + const WithLoadedUser = ({ currentUserLoading, ...props }: WithUserProps) => { + return currentUserLoading ? null : ; + }; + + return withUser(WithLoadedUser); +}; + +const IfNotLoggedInComponent: React.FunctionComponent = ({ + currentUser, + children, + refetchCurrentUser, + ...restProps +}) => + !currentUser + ? React.cloneElement(children, { + ...restProps + }) + : null; + +const IfNotLoggedIn: React.ComponentType = withLoadedUser(IfNotLoggedInComponent); + +export { withUser, hasRole, withLoadedUser, IfNotLoggedIn }; From 907aad71167368b78daeaad4fe82cff7b1823ea2 Mon Sep 17 00:00:00 2001 From: Ivan Isakov Date: Tue, 7 May 2019 09:01:11 +0300 Subject: [PATCH 005/104] Add Data root component --- .../containers/DataRootComponent.native.tsx | 9 +++++++++ .../client-react/containers/DataRootComponent.tsx | 11 +++++++++++ 2 files changed, 20 insertions(+) create mode 100644 modules/user/client-react/containers/DataRootComponent.native.tsx create mode 100644 modules/user/client-react/containers/DataRootComponent.tsx diff --git a/modules/user/client-react/containers/DataRootComponent.native.tsx b/modules/user/client-react/containers/DataRootComponent.native.tsx new file mode 100644 index 0000000..9684a75 --- /dev/null +++ b/modules/user/client-react/containers/DataRootComponent.native.tsx @@ -0,0 +1,9 @@ +import * as React from 'react'; + +class DataRootComponent extends React.Component { + public render() { + return this.props.children; + } +} + +export default DataRootComponent; diff --git a/modules/user/client-react/containers/DataRootComponent.tsx b/modules/user/client-react/containers/DataRootComponent.tsx new file mode 100644 index 0000000..4dc0f40 --- /dev/null +++ b/modules/user/client-react/containers/DataRootComponent.tsx @@ -0,0 +1,11 @@ +import * as React from 'react'; + +interface DataRootComponent { + children?: Element | any; +} + +const DataRootComponent: React.FunctionComponent = props => { + return props.children; +}; + +export default DataRootComponent; From 84ed7076dd6a1eaf65395496f971d57159b2c7b6 Mon Sep 17 00:00:00 2001 From: Ivan Isakov Date: Tue, 7 May 2019 09:31:28 +0300 Subject: [PATCH 006/104] Add config --- config/auth.js | 8 ++++++++ config/index.js | 1 + 2 files changed, 9 insertions(+) create mode 100644 config/auth.js diff --git a/config/auth.js b/config/auth.js new file mode 100644 index 0000000..68329a8 --- /dev/null +++ b/config/auth.js @@ -0,0 +1,8 @@ +export default { + password: { + requireEmailConfirmation: false, + sendPasswordChangesEmail: false, + minLength: 8, + enabled: true + } +}; diff --git a/config/index.js b/config/index.js index 1cab3e5..a420c20 100644 --- a/config/index.js +++ b/config/index.js @@ -1,4 +1,5 @@ export { default as app } from './app'; +export { default as auth } from './auth'; export { default as db } from './db'; export { default as mailer } from './mailer'; export { default as analytics } from './analytics'; From 236aff7ade5a53d7b120ade3e025078216f6d16c Mon Sep 17 00:00:00 2001 From: Ivan Isakov Date: Tue, 7 May 2019 09:32:32 +0300 Subject: [PATCH 007/104] Add universalcoockie interface --- modules/module/client-react/BaseModule.ts | 6 +++++- .../user/client-react/containers/AuthBase.tsx | 16 +++------------- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/modules/module/client-react/BaseModule.ts b/modules/module/client-react/BaseModule.ts index f9cdda3..8fd9aca 100644 --- a/modules/module/client-react/BaseModule.ts +++ b/modules/module/client-react/BaseModule.ts @@ -4,10 +4,14 @@ import { ReducersMapObject } from 'redux'; import CommonModule, { CommonModuleShape } from '@restapp/module-common'; +interface UniversalCookieRequest { + universalCookies?: any; +} + /** * A function that creates React Element that wraps root element of a React tree */ -type RootComponentFactory = (req: Request) => React.ReactElement; +type RootComponentFactory = (req: Request & UniversalCookieRequest) => React.ReactElement; /** * Common module interface for React and React Native feature modules. diff --git a/modules/user/client-react/containers/AuthBase.tsx b/modules/user/client-react/containers/AuthBase.tsx index cf6f1d1..cbaefcf 100644 --- a/modules/user/client-react/containers/AuthBase.tsx +++ b/modules/user/client-react/containers/AuthBase.tsx @@ -2,9 +2,7 @@ import * as React from 'react'; import { RouteProps } from 'react-router'; import { History } from 'history'; import { connect } from 'react-redux'; -import { CURRENT_USER } from '../actions'; import { User, UserRole } from '..'; -import { ActionType } from '../reducers'; export interface WithUserProps extends RouteProps { currentUser?: User; @@ -50,17 +48,9 @@ const withLoadedUser = (Component: React.ComponentType) => { return withUser(WithLoadedUser); }; -const IfNotLoggedInComponent: React.FunctionComponent = ({ - currentUser, - children, - refetchCurrentUser, - ...restProps -}) => - !currentUser - ? React.cloneElement(children, { - ...restProps - }) - : null; +const IfNotLoggedInComponent: React.FunctionComponent = ({ currentUser, children }) => { + return !currentUser ? React.cloneElement(children, {}) : null; +}; const IfNotLoggedIn: React.ComponentType = withLoadedUser(IfNotLoggedInComponent); From 4393437ae1d83f2600075a9fb53b2f292bcc2295 Mon Sep 17 00:00:00 2001 From: Ivan Isakov Date: Tue, 7 May 2019 09:33:48 +0300 Subject: [PATCH 008/104] Add register functionality with reducers --- modules/user/client-react/actions/index.ts | 3 + modules/user/client-react/actions/register.ts | 6 + .../client-react/components/RegisterForm.tsx | 70 +++++++++++ .../client-react/components/RegisterView.tsx | 55 +++++++++ .../user/client-react/containers/Register.tsx | 46 +++++++ modules/user/client-react/index.tsx | 112 ++++++++++++++++++ modules/user/client-react/reducers/index.ts | 52 ++++++++ packages/client/src/modules.ts | 3 +- 8 files changed, 346 insertions(+), 1 deletion(-) create mode 100644 modules/user/client-react/actions/index.ts create mode 100644 modules/user/client-react/actions/register.ts create mode 100644 modules/user/client-react/components/RegisterForm.tsx create mode 100644 modules/user/client-react/components/RegisterView.tsx create mode 100644 modules/user/client-react/containers/Register.tsx create mode 100644 modules/user/client-react/index.tsx create mode 100644 modules/user/client-react/reducers/index.ts diff --git a/modules/user/client-react/actions/index.ts b/modules/user/client-react/actions/index.ts new file mode 100644 index 0000000..98c39af --- /dev/null +++ b/modules/user/client-react/actions/index.ts @@ -0,0 +1,3 @@ +import REGISTER from './register'; + +export { REGISTER }; diff --git a/modules/user/client-react/actions/register.ts b/modules/user/client-react/actions/register.ts new file mode 100644 index 0000000..7fd53e1 --- /dev/null +++ b/modules/user/client-react/actions/register.ts @@ -0,0 +1,6 @@ +import axios from 'axios'; +import { RegisterSubmitProps } from '..'; + +const REGISTER = async (value: RegisterSubmitProps) => axios.post(`${__API_URL__}/register`, { ...value }); + +export default REGISTER; diff --git a/modules/user/client-react/components/RegisterForm.tsx b/modules/user/client-react/components/RegisterForm.tsx new file mode 100644 index 0000000..e4067b5 --- /dev/null +++ b/modules/user/client-react/components/RegisterForm.tsx @@ -0,0 +1,70 @@ +import * as React from 'react'; +import { withFormik } from 'formik'; +import { isFormError, FieldAdapter as Field } from '@restapp/forms-client-react'; +import { translate } from '@restapp/i18n-client-react'; + +import { match, email, minLength, required, validate } from '@restapp/validation-common-react'; +import { Form, RenderField, Button, Alert } from '@restapp/look-client-react'; + +import { FormProps, RegisterSubmitProps } from '..'; +import settings from '../../../../settings'; + +const registerFormSchema = { + username: [required, minLength(3)], + email: [required, email], + password: [required, minLength(settings.auth.password.minLength)], + passwordConfirmation: [match('password'), required, minLength(settings.auth.password.minLength)] +}; + +const RegisterForm = ({ values, handleSubmit, submitting, errors, t }: FormProps) => { + return ( +
+ + + + +
+ {errors && errors.errorMsg && {errors.errorMsg}} + +
+ + ); +}; + +const RegisterFormWithFormik = withFormik, RegisterSubmitProps>({ + mapPropsToValues: () => ({ username: '', email: '', password: '', passwordConfirmation: '' }), + validate: values => validate(values, registerFormSchema), + async handleSubmit(values, { setErrors, props: { onSubmit } }) { + onSubmit(values).catch((e: any) => { + if (isFormError(e)) { + setErrors(e.errors); + } else { + throw e; + } + }); + }, + enableReinitialize: true, + displayName: 'SignUpForm' // helps with React DevTools +}); + +export default translate('user')(RegisterFormWithFormik(RegisterForm)); diff --git a/modules/user/client-react/components/RegisterView.tsx b/modules/user/client-react/components/RegisterView.tsx new file mode 100644 index 0000000..5578ca5 --- /dev/null +++ b/modules/user/client-react/components/RegisterView.tsx @@ -0,0 +1,55 @@ +import * as React from 'react'; +import Helmet from 'react-helmet'; +import { translate, TranslateFunction } from '@restapp/i18n-client-react'; +import { LayoutCenter, PageLayout, Card, CardGroup, CardTitle, CardText } from '@restapp/look-client-react'; + +import RegisterForm from '../components/RegisterForm'; + +import settings from '../../../../settings'; + +import { RegisterSubmitProps } from '..'; + +interface RegisterViewProps { + t: TranslateFunction; + onSubmit: (values: RegisterSubmitProps) => void; + isRegistered: boolean; +} + +const RegisterView = ({ t, onSubmit, isRegistered }: RegisterViewProps) => { + const renderMetaData = () => ( + + ); + + const renderConfirmationModal = () => ( + + + {t('reg.confirmationMsgTitle')} + {t('reg.confirmationMsgBody')} + + + ); + + return ( + + {renderMetaData()} + +

{t('reg.form.title')}

+ {isRegistered && settings.auth.password.requireEmailConfirmation ? ( + renderConfirmationModal() + ) : ( + + )} +
+
+ ); +}; + +export default translate('user')(RegisterView); diff --git a/modules/user/client-react/containers/Register.tsx b/modules/user/client-react/containers/Register.tsx new file mode 100644 index 0000000..b514064 --- /dev/null +++ b/modules/user/client-react/containers/Register.tsx @@ -0,0 +1,46 @@ +import * as React from 'react'; +import { connect } from 'react-redux'; +import { translate } from '@restapp/i18n-client-react'; +import { FormError } from '@restapp/forms-client-react'; + +import RegisterView from '../components/RegisterView'; + +import { CommonProps, RegisterSubmitProps } from '..'; +import { REGISTER } from '../actions'; +import settings from '../../../../settings'; + +interface RegisterProps extends CommonProps { + register: (values: RegisterSubmitProps) => void; +} + +const Register: React.FunctionComponent = props => { + const { t, register, history } = props; + + const [isRegistered, setIsRegistered] = React.useState(false); + + const onSubmit = async (values: RegisterSubmitProps) => { + try { + await register(values); + if (!settings.auth.password.requireEmailConfirmation) { + history.push('/'); + } else { + setIsRegistered(true); + } + } catch (e) { + throw new FormError(t('reg.errorMsg'), e); + } + }; + + return ; +}; + +export default connect( + _state => ({}), + dispatch => ({ + register: (value: RegisterSubmitProps) => + dispatch({ + type: null, + request: () => REGISTER(value) + }) + }) +)(translate('user')(Register)); diff --git a/modules/user/client-react/index.tsx b/modules/user/client-react/index.tsx new file mode 100644 index 0000000..8014aad --- /dev/null +++ b/modules/user/client-react/index.tsx @@ -0,0 +1,112 @@ +import * as React from 'react'; +import * as H from 'history'; +import { CookiesProvider } from 'react-cookie'; +import { NavLink } from 'react-router-dom'; +import { translate, TranslateFunction } from '@restapp/i18n-client-react'; +import { MenuItem } from '@restapp/look-client-react'; +import ClientModule from '@restapp/module-client-react'; +import { FormikErrors } from 'formik'; + +import resources from './locales'; +import DataRootComponent from './containers/DataRootComponent'; +import Register from './containers/Register'; + +import reducers from './reducers'; + +import { AuthRoute, IfNotLoggedIn } from './containers/Auth'; + +export enum UserRole { + admin = 'admin', + user = 'user' +} + +export interface UserProfile { + fullName?: string; + firstName?: string; + lastName?: string; +} + +export interface User { + id?: number | string; + username: string; + role: UserRole; + isActive: boolean; + email: string; + profile?: UserProfile; + auth?: any; +} + +export interface LoginSubmitProps { + usernameOrEmail: string; + password: string; +} +export interface ResetPasswordSubmitProps { + password: string; + passwordConfirmation: string; +} +export interface ForgotPasswordSubmitProps { + email: string; +} + +export interface RegisterSubmitProps { + username: string; + email: string; + password: string; + passwordConfirmation: string; +} + +export interface CommonProps { + t?: TranslateFunction; + history?: H.History; +} + +interface HandleSubmitProps

{ + setErrors: (errors: FormikErrors

) => void; + props: P; +} + +interface Errors { + errorMsg?: string; +} +export interface FormProps { + handleSubmit: (values: V, props: HandleSubmitProps) => void; + onSubmit: (values: V) => Promise | void | any; + submitting?: boolean; + errors: Errors; + values: V; + t?: TranslateFunction; +} + +export interface OrderBy { + column: string; + order: string; +} + +export interface Filter { + searchText: string; + role: string; + isActive: boolean; +} + +export * from './containers/Auth'; + +const NavLinkLoginWithI18n = translate('user')(({ t }: any) => ( + + {t('navLink.signUp')} + +)); + +export default new ClientModule({ + route: [], + navItemRight: [ + + + + + + ], + localization: [{ ns: 'user', resources }], + reducer: [{ user: reducers }], + dataRootComponent: [DataRootComponent], + rootComponentFactory: [req => (req ? : )] +}); diff --git a/modules/user/client-react/reducers/index.ts b/modules/user/client-react/reducers/index.ts new file mode 100644 index 0000000..11df66c --- /dev/null +++ b/modules/user/client-react/reducers/index.ts @@ -0,0 +1,52 @@ +import { User } from '..'; + +export enum ActionType { + SET_CURRENT_USER = 'SET_CURRENT_USER', + CLEAR_CURRENT_USER = 'CLEAR_CURRENT_USER', + SET_LOADING = 'SET_LOADING' +} + +export interface UserModuleState { + currentUser: User; + loading: boolean; +} + +export interface UserModuleActionProps { + type: ActionType | ActionType[]; + payload?: any; + request?: () => Promise; + [key: string]: any; +} + +const defaultState: UserModuleState = { + currentUser: null, + loading: false +}; + +export default function(state = defaultState, action: UserModuleActionProps) { + switch (action.type) { + case ActionType.SET_LOADING: + return { + ...state, + loading: true + }; + + case ActionType.CLEAR_CURRENT_USER: + return { + ...state, + currentUser: null, + loading: false, + ...action.payload + }; + + case ActionType.SET_CURRENT_USER: + return { + ...state, + currentUser: action.payload, + loading: false + }; + + default: + return state; + } +} diff --git a/packages/client/src/modules.ts b/packages/client/src/modules.ts index 85cdc5c..3e3038d 100644 --- a/packages/client/src/modules.ts +++ b/packages/client/src/modules.ts @@ -1,4 +1,5 @@ import welcome from '@restapp/welcome-client-react'; +import user from '@restapp/user-client-react'; import core from '@restapp/core-client-react'; import look from '@restapp/look-client-react'; import i18n from '@restapp/i18n-client-react'; @@ -9,6 +10,6 @@ import '@restapp/favicon-common'; const pageNotFound = require('@restapp/page-not-found-client-react').default; -const modules = new ClientModule(welcome, look, validation, defaultRouter, i18n, pageNotFound, core); +const modules = new ClientModule(user, welcome, look, validation, defaultRouter, i18n, pageNotFound, core); export default modules; From e404dda868671dc7175141e279b6919228b0395b Mon Sep 17 00:00:00 2001 From: Ivan Isakov Date: Tue, 7 May 2019 09:57:50 +0300 Subject: [PATCH 009/104] Add register functionality for mobile app --- .../components/RegisterForm.native.tsx | 102 ++++++++++++ .../components/RegisterView.native.tsx | 69 ++++++++ .../containers/Register.native.tsx | 70 ++++++++ .../containers/UserScreenNavigator.native.tsx | 89 ++++++++++ modules/user/client-react/index.native.tsx | 156 ++++++++++++++++++ packages/mobile/src/modules.ts | 3 +- 6 files changed, 488 insertions(+), 1 deletion(-) create mode 100644 modules/user/client-react/components/RegisterForm.native.tsx create mode 100644 modules/user/client-react/components/RegisterView.native.tsx create mode 100644 modules/user/client-react/containers/Register.native.tsx create mode 100644 modules/user/client-react/containers/UserScreenNavigator.native.tsx create mode 100644 modules/user/client-react/index.native.tsx diff --git a/modules/user/client-react/components/RegisterForm.native.tsx b/modules/user/client-react/components/RegisterForm.native.tsx new file mode 100644 index 0000000..9623215 --- /dev/null +++ b/modules/user/client-react/components/RegisterForm.native.tsx @@ -0,0 +1,102 @@ +import * as React from 'react'; +import { View, StyleSheet } from 'react-native'; +import { withFormik } from 'formik'; +import KeyboardSpacer from 'react-native-keyboard-spacer'; +import { isFormError, FieldAdapter as Field } from '@restapp/forms-client-react'; +import { translate } from '@restapp/i18n-client-react'; + +import { RenderField, Button, primary, FormView } from '@restapp/look-client-react-native'; +import { placeholderColor, submit } from '@restapp/look-client-react-native/styles'; +import { match, email, minLength, required, validate } from '@restapp/validation-common-react'; + +import settings from '../../../../settings'; + +import { FormProps, RegisterSubmitProps } from '../index.native'; + +const registerFormSchema = { + username: [required, minLength(3)], + email: [required, email], + password: [required, minLength(settings.auth.password.minLength)], + passwordConfirmation: [match('password'), required, minLength(settings.auth.password.minLength)] +}; + +const RegisterForm = ({ values, handleSubmit, t }: FormProps) => { + return ( + + + + + + + + + + + + + ); +}; + +const RegisterFormWithFormik = withFormik, RegisterSubmitProps>({ + mapPropsToValues: () => ({ username: '', email: '', password: '', passwordConfirmation: '' }), + validate: values => validate(values, registerFormSchema), + async handleSubmit(values, { setErrors, props: { onSubmit } }) { + onSubmit(values).catch((e: any) => { + if (isFormError(e)) { + setErrors(e.errors); + } else { + throw e; + } + }); + }, + enableReinitialize: true, + displayName: 'SignUpForm' // helps with React DevTools +}); + +const styles = StyleSheet.create({ + submit, + formView: { + flex: 1, + alignSelf: 'stretch' + }, + formContainer: { + paddingHorizontal: 20, + flex: 1, + justifyContent: 'center' + } +}); + +export default translate('user')(RegisterFormWithFormik(RegisterForm)); diff --git a/modules/user/client-react/components/RegisterView.native.tsx b/modules/user/client-react/components/RegisterView.native.tsx new file mode 100644 index 0000000..273c367 --- /dev/null +++ b/modules/user/client-react/components/RegisterView.native.tsx @@ -0,0 +1,69 @@ +import * as React from 'react'; +import { Button, primary } from '@restapp/look-client-react-native'; +import { View, Text, StyleSheet } from 'react-native'; +import { TranslateFunction, translate } from '@restapp/i18n-client-react'; + +import RegisterForm from '../components/RegisterForm.native'; + +import { RegisterSubmitProps } from '../index.native'; + +interface RegisterViewProps { + t: TranslateFunction; + onSubmit: (values: RegisterSubmitProps) => void; + isRegistered: boolean; + hideModal: () => void; +} + +class RegisterView extends React.PureComponent { + public renderModal = () => { + const { t, hideModal } = this.props; + return ( + + {t('reg.confirmationMsgTitle')} + {t('reg.confirmationMsgBody')} + + + + + ); + }; + + public render() { + const { onSubmit, isRegistered } = this.props; + return ( + {isRegistered ? this.renderModal() : } + ); + } +} + +const styles = StyleSheet.create({ + container: { + justifyContent: 'center', + alignItems: 'stretch', + flex: 1 + }, + modalWrapper: { + flex: 1, + justifyContent: 'center', + alignItems: 'stretch', + margin: 20 + }, + modalTitle: { + fontSize: 18, + fontWeight: 'bold', + marginBottom: 20, + textAlign: 'center' + }, + modalBody: { + fontSize: 16, + marginBottom: 20 + }, + button: { + flex: 1, + paddingTop: 10 + } +}); + +export default translate('user')(RegisterView); diff --git a/modules/user/client-react/containers/Register.native.tsx b/modules/user/client-react/containers/Register.native.tsx new file mode 100644 index 0000000..80eb7b4 --- /dev/null +++ b/modules/user/client-react/containers/Register.native.tsx @@ -0,0 +1,70 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { compose } from 'redux'; +import { translate } from '@restapp/i18n-client-react'; +import { FormError } from '@restapp/forms-client-react'; +import RegisterView from '../components/RegisterView.native'; + +import settings from '../../../../settings'; +import { REGISTER } from '../actions'; +import { CommonProps, RegisterSubmitProps } from '../index.native'; + +interface RegisterProps extends CommonProps { + register: (values: RegisterSubmitProps) => void; +} + +interface RegisterState { + isRegistered: boolean; +} + +class Register extends React.Component { + public state = { + isRegistered: false + }; + + public onSubmit = async (values: RegisterSubmitProps) => { + const { t, register, navigation } = this.props; + + try { + await register(values); + if (!settings.auth.password.requireEmailConfirmation) { + navigation.goBack(); + } else { + this.setState({ isRegistered: true }); + } + } catch (e) { + throw new FormError(t('reg.errorMsg'), e); + } + }; + + public hideModal = () => { + this.props.navigation.goBack(); + }; + + public toggleModal = () => { + this.setState(prevState => ({ isRegistered: !prevState.isRegistered })); + }; + + public render() { + const { isRegistered } = this.state; + return ( + + ); + } +} + +const withConnect = connect( + _state => ({}), + dispatch => ({ + register: (value: RegisterSubmitProps) => + dispatch({ + type: null, + request: () => REGISTER(value) + }) + }) +); + +export default compose( + translate('user'), + withConnect(Register) +); diff --git a/modules/user/client-react/containers/UserScreenNavigator.native.tsx b/modules/user/client-react/containers/UserScreenNavigator.native.tsx new file mode 100644 index 0000000..a15bb09 --- /dev/null +++ b/modules/user/client-react/containers/UserScreenNavigator.native.tsx @@ -0,0 +1,89 @@ +import { createAppContainer, createDrawerNavigator } from 'react-navigation'; +import React from 'react'; +import { pickBy } from 'lodash'; +import { compose } from 'redux'; +import { DrawerComponent } from '@restapp/look-client-react-native'; + +import { UserRole } from '../index.native'; +import { withUser } from './Auth'; + +interface User { + id: number | string; + role: UserRole; +} + +interface UserScreenNavigator { + currentUser: User; + context: any; + currentUserLoading: boolean; + routeConfigs: any; +} + +class UserScreenNavigator extends React.Component { + public shouldComponentUpdate(nextProps: UserScreenNavigator) { + const { currentUserLoading, currentUser } = this.props; + /** + * After a user edits the profile the CurrentUser being updated in the State as well. + * That leads to the Navigator re-rendering and, as a result, takes the user back to the initial route. + * In order to let the user get back to his/her profile we need to prevent the Navigator + * re-render action after profile was edited + */ + return !( + !currentUserLoading && + currentUser && + nextProps.currentUser && + currentUser.id === nextProps.currentUser.id && + currentUser.role === nextProps.currentUser.role + ); + } + + public navItemsFilter = () => { + const { currentUser, currentUserLoading, routeConfigs } = this.props; + + const userFilter = (value: any) => { + if (!value.userInfo) { + return true; + } + const { showOnLogin, role } = value.userInfo; + return showOnLogin && (!role || (Array.isArray(role) ? role : [role]).includes(currentUser.role)); + }; + + const guestFilter = (value: any) => !value.userInfo || (value.userInfo && !value.userInfo.showOnLogin); + + return pickBy(routeConfigs, currentUser && !currentUserLoading ? userFilter : guestFilter); + }; + public getInitialRoute = () => { + const { currentUser } = this.props; + return currentUser ? 'Profile' : 'Login'; + }; + + public render() { + const MainScreenNavigatorComponent = createAppContainer( + createDrawerNavigator( + { ...this.navItemsFilter() }, + { + // eslint-disable-next-line + contentComponent: props => , + initialRouteName: this.getInitialRoute() + } + ) + ); + + return ; + } +} + +const drawerNavigator: any = (routeConfigs: any) => { + const withRoutes = (Component: React.ComponentType) => { + const ownProps = { routeConfigs }; + const WithRoutesComponent = ({ ...props }) => ; + return WithRoutesComponent; + }; + + return compose( + withUser, + withRoutes + )(UserScreenNavigator); +}; + +export default drawerNavigator; diff --git a/modules/user/client-react/index.native.tsx b/modules/user/client-react/index.native.tsx new file mode 100644 index 0000000..05c575a --- /dev/null +++ b/modules/user/client-react/index.native.tsx @@ -0,0 +1,156 @@ +import * as React from 'react'; +import { + createStackNavigator, + NavigationContainer, + NavigationScreenProp, + NavigationRoute, + NavigationParams +} from 'react-navigation'; +import { translate, TranslateFunction } from '@restapp/i18n-client-react'; +import { HeaderTitle, IconButton } from '@restapp/look-client-react-native'; +import ClientModule from '@restapp/module-client-react-native'; +import { FormikErrors } from 'formik'; +import resources from './locales'; +import DataRootComponent from './containers/DataRootComponent.native'; +import UserScreenNavigator from './containers/UserScreenNavigator.native'; +import Register from './containers/Register.native'; + +import reducers from './reducers'; + +export enum UserRole { + admin = 'admin', + user = 'user' +} + +export interface UserProfile { + fullName?: string; + firstName?: string; + lastName?: string; +} + +export interface User { + id?: number | string; + username: string; + role: UserRole; + isActive: boolean; + email: string; + profile?: UserProfile; + auth?: any; +} + +export interface NavigationOptionsProps { + navigation: NavigationScreenProp, NavigationParams>; +} + +export interface CommonProps extends NavigationOptionsProps { + error?: string; + t?: TranslateFunction; +} + +export interface LoginSubmitProps { + usernameOrEmail: string; + password: string; +} + +export interface ResetPasswordSubmitProps { + password: string; + passwordConfirmation: string; +} + +export interface ForgotPasswordSubmitProps { + email: string; +} + +export interface RegisterSubmitProps { + username: string; + email: string; + password: string; + passwordConfirmation: string; +} + +interface HandleSubmitProps

{ + setErrors: (errors: FormikErrors

) => void; + props: P; +} + +interface Errors { + errorMsg?: string; +} + +export interface FormProps { + handleSubmit: (values: V, props: HandleSubmitProps) => void; + onSubmit: (values: V) => Promise | void | any; + submitting?: boolean; + errors: Errors; + values: V; + t?: TranslateFunction; +} + +export interface OrderBy { + column: string; + order: string; +} + +export interface Filter { + searchText: string; + role: string; + isActive: boolean; +} + +class RegisterScreen extends React.Component { + public static navigationOptions = () => ({ + headerTitle: , + headerForceInset: {} + }); + public render() { + return ; + } +} + +const AuthScreen = createStackNavigator( + { + Register: { screen: RegisterScreen } + }, + { + cardStyle: { + backgroundColor: '#fff' + }, + navigationOptions: { + headerStyle: { backgroundColor: '#fff' } + } + } +); + +export * from './containers/Auth.native'; + +const HeaderTitleWithI18n = translate('user')(HeaderTitle); + +export const ref: { navigator: NavigationContainer } = { + navigator: null +}; + +const MainScreenNavigator = () => { + const Navigator = ref.navigator; + return ; +}; + +export default new ClientModule({ + drawerItem: [ + { + Login: { + screen: AuthScreen, + userInfo: { + showOnLogin: false + }, + navigationOptions: { + drawerLabel: + } + } + } + ], + localization: [{ ns: 'user', resources }], + router: , + reducer: [{ user: reducers }], + dataRootComponent: [DataRootComponent], + onAppCreate: [(module: ClientModule) => (ref.navigator = UserScreenNavigator(module.drawerItems))] +}); diff --git a/packages/mobile/src/modules.ts b/packages/mobile/src/modules.ts index ba8e395..09a076e 100644 --- a/packages/mobile/src/modules.ts +++ b/packages/mobile/src/modules.ts @@ -1,4 +1,5 @@ import welcome from '@restapp/welcome-client-react'; +import user from '@restapp/user-client-react'; import core from '@restapp/core-client-react-native'; import i18n from '@restapp/i18n-client-react'; import validation from '@restapp/validation-common-react'; @@ -6,6 +7,6 @@ import defaultRouter from '@restapp/router-client-react-native'; import ClientModule from '@restapp/module-client-react-native'; -const modules = new ClientModule(welcome, validation, defaultRouter, i18n, core); +const modules = new ClientModule(user, welcome, validation, defaultRouter, i18n, core); export default modules; From 0f369c29b5d574254145c93734161f45bb887ac6 Mon Sep 17 00:00:00 2001 From: Alexandr Belokur Date: Tue, 7 May 2019 10:08:11 +0300 Subject: [PATCH 010/104] Update ServerModule --- modules/module/server-ts/ServerModule.ts | 45 +++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/modules/module/server-ts/ServerModule.ts b/modules/module/server-ts/ServerModule.ts index 7aba114..056aec7 100644 --- a/modules/module/server-ts/ServerModule.ts +++ b/modules/module/server-ts/ServerModule.ts @@ -1,5 +1,6 @@ -import { Express } from 'express'; +import { isEmpty } from 'lodash'; import CommonModule, { CommonModuleShape } from '@restapp/module-common'; +import { Express, Request, Response } from 'express'; /** * Create GraphQL context function params @@ -30,6 +31,22 @@ type CreateContextFunc = (props: CreateContextFuncProps) => { [key: string]: any */ type MiddlewareFunc = (app: Express, appContext: { [key: string]: any }) => void; +/** + * A function with reuqest. + * + * @param req HTTP request + * @param res HTTP response + * @param next following handler + */ +type RequestHandler = (req: Request, res: Response, next: any) => void; + +export enum RestMethod { + POST = 'post', + GET = 'get', + PUT = 'put', + DELETE = 'delete' +} + /** * Server feature modules interface */ @@ -40,6 +57,14 @@ export interface ServerModuleShape extends CommonModuleShape { beforeware?: MiddlewareFunc[]; // A list of functions to register normal-priority middlewares middleware?: MiddlewareFunc[]; + accessMiddleware?: RequestHandler; + apiRouteParams?: Array<{ + method: RestMethod; + route: string; + middleware?: RequestHandler[]; + controller: RequestHandler; + isAuthRoute?: boolean; + }>; } interface ServerModule extends ServerModuleShape {} @@ -72,6 +97,24 @@ class ServerModule extends CommonModule { } return context; } + + public get apiRoutes() { + return this.apiRouteParams.map(({ method, route, controller, isAuthRoute, middleware }) => { + return (app: Express, modules: ServerModule) => { + const handlers = []; + + if (isAuthRoute) { + handlers.push(modules.accessMiddleware); + } + if (!isEmpty(middleware)) { + handlers.push(...middleware); + } + handlers.push(controller); + + app[method](`/api/${route}`, ...handlers); + }; + }); + } } export default ServerModule; From c5993f43d31428169153bdc12e525158617111cb Mon Sep 17 00:00:00 2001 From: Alexandr Belokur Date: Tue, 7 May 2019 10:27:52 +0300 Subject: [PATCH 011/104] Add initial setup for server user module --- modules/user/server-ts/index.ts | 3 + .../server-ts/locales/en/translations.json | 23 ++ modules/user/server-ts/locales/index.ts | 5 + .../server-ts/locales/ru/translations.json | 23 ++ modules/user/server-ts/migration/002_user.js | 96 ++++++ modules/user/server-ts/package.json | 5 + modules/user/server-ts/seeds/.eslintrc | 5 + modules/user/server-ts/seeds/002_user.js | 36 +++ modules/user/server-ts/sql.ts | 285 ++++++++++++++++++ packages/server/package.json | 4 +- packages/server/src/modules.ts | 3 +- yarn.lock | 14 +- 12 files changed, 493 insertions(+), 9 deletions(-) create mode 100644 modules/user/server-ts/index.ts create mode 100644 modules/user/server-ts/locales/en/translations.json create mode 100644 modules/user/server-ts/locales/index.ts create mode 100644 modules/user/server-ts/locales/ru/translations.json create mode 100644 modules/user/server-ts/migration/002_user.js create mode 100644 modules/user/server-ts/package.json create mode 100644 modules/user/server-ts/seeds/.eslintrc create mode 100644 modules/user/server-ts/seeds/002_user.js create mode 100644 modules/user/server-ts/sql.ts diff --git a/modules/user/server-ts/index.ts b/modules/user/server-ts/index.ts new file mode 100644 index 0000000..20bcfdc --- /dev/null +++ b/modules/user/server-ts/index.ts @@ -0,0 +1,3 @@ +import ServerModule from '@restapp/module-server-ts'; + +export default new ServerModule(); diff --git a/modules/user/server-ts/locales/en/translations.json b/modules/user/server-ts/locales/en/translations.json new file mode 100644 index 0000000..b67acdb --- /dev/null +++ b/modules/user/server-ts/locales/en/translations.json @@ -0,0 +1,23 @@ +{ + "accessDenied": "Access Denied", + "usernameIsExisted": "Username already exists.", + "emailIsExisted": "E-mail already exists.", + "passwordLength": "Password must be {{length}} characters or more.", + "userIsNotExisted": "User does not exist.", + "userCannotDeleteYourself": "You can not delete your self.", + "userCouldNotDeleted": "Could not delete user. Please try again later.", + "auth": { + "password": { + "validPasswordEmail": "Please enter a valid username or e-mail.", + "emailConfirmation": "Please confirm your e-mail first.", + "validPassword": "Please enter a valid password.", + "usernameIsExisted": "Username already exists", + "emailIsExisted": "E-mail already exists.", + "passwordsIsNotMatch": "Passwords do not match.", + "passwordLength": "Password must be {{length}} characters or more.", + "invalidToken": "Invalid token", + "forgotPassword": "Instructions were send to your email", + "resestPassword": "Your password has been updated succesfully" + } + } +} \ No newline at end of file diff --git a/modules/user/server-ts/locales/index.ts b/modules/user/server-ts/locales/index.ts new file mode 100644 index 0000000..d9fb121 --- /dev/null +++ b/modules/user/server-ts/locales/index.ts @@ -0,0 +1,5 @@ +/* + * The index.js can be empty, it's just needed to point the loader to the root directory of the locales. + * https://github.com/alienfast/i18next-loader#option-2-use-with-import-syntax + */ +export default {}; diff --git a/modules/user/server-ts/locales/ru/translations.json b/modules/user/server-ts/locales/ru/translations.json new file mode 100644 index 0000000..457fff7 --- /dev/null +++ b/modules/user/server-ts/locales/ru/translations.json @@ -0,0 +1,23 @@ +{ + "accessDenied": "Доступ запрещен", + "usernameIsExisted": "Данное имя пользователя уже используется.", + "emailIsExisted": "Данный e-mail уже используется.", + "passwordLength": "Длина пароля должна составлять {{length}} символов и больше.", + "userIsNotExisted": "Такого пользователя не существует.", + "userCannotDeleteYourself": "Вы не можете удалить себя", + "userCouldNotDeleted": "Невозможно удалить пользователя. Побробуйте повторить попытку позже.", + "auth": { + "password": { + "validPasswordEmail": "Пожалуйста, введите Ваш username или e-mail.", + "emailConfirmation": "Пожалуйста, подтвердите Ваш e-mail", + "validPassword": "Пожалуйста, введите действительный пароль.", + "usernameIsExisted": "Данное имя пользователя уже используется", + "emailIsExisted": "Данный e-mail уже используется.", + "passwordsIsNotMatch": "Пароли не совпадают.", + "passwordLength": "Длина пароля должна составлять {{length}} символов и больше.", + "invalidToken": "Не действительный токен", + "forgotPassword": "Интсрукции были отправлены на ваш email", + "resestPassword": "Ваш пароль был успешно обновлен" + } + } +} \ No newline at end of file diff --git a/modules/user/server-ts/migration/002_user.js b/modules/user/server-ts/migration/002_user.js new file mode 100644 index 0000000..d60f829 --- /dev/null +++ b/modules/user/server-ts/migration/002_user.js @@ -0,0 +1,96 @@ +exports.up = function(knex, Promise) { + return Promise.all([ + knex.schema.createTable('user', table => { + table.increments(); + table.string('username').unique(); + table.string('email').unique(); + table.string('password_hash'); + table.string('role').defaultTo('user'); + table.boolean('is_active').defaultTo(false); + table.timestamps(false, true); + }), + knex.schema.createTable('user_profile', table => { + table.increments(); + table.string('first_name'); + table.string('last_name'); + table + .integer('user_id') + .unsigned() + .references('id') + .inTable('user') + .onDelete('CASCADE'); + table.timestamps(false, true); + }), + knex.schema.createTable('auth_certificate', table => { + table.increments(); + table.string('serial').unique(); + table + .integer('user_id') + .unsigned() + .references('id') + .inTable('user') + .onDelete('CASCADE'); + table.timestamps(false, true); + }), + knex.schema.createTable('auth_facebook', table => { + table.increments(); + table.string('fb_id').unique(); + table.string('display_name'); + table + .integer('user_id') + .unsigned() + .references('id') + .inTable('user') + .onDelete('CASCADE'); + table.timestamps(false, true); + }), + knex.schema.createTable('auth_google', table => { + table.increments(); + table.string('google_id').unique(); + table.string('display_name'); + table + .integer('user_id') + .unsigned() + .references('id') + .inTable('user') + .onDelete('CASCADE'); + table.timestamps(false, true); + }), + knex.schema.createTable('auth_github', table => { + table.increments(); + table.string('gh_id').unique(); + table.string('display_name'); + table + .integer('user_id') + .unsigned() + .references('id') + .inTable('user') + .onDelete('CASCADE'); + table.timestamps(false, true); + }), + knex.schema.createTable('auth_linkedin', table => { + table.increments(); + table.string('ln_id').unique(); + table.string('display_name'); + table + .integer('user_id') + .unsigned() + .references('id') + .inTable('user') + .onDelete('CASCADE'); + table.timestamps(false, true); + }) + ]); +}; + +exports.down = function(knex, Promise) { + return Promise.all([ + knex.schema.dropTable('auth_certificate'), + knex.schema.dropTable('auth_facebook'), + knex.schema.dropTable('auth_google'), + knex.schema.dropTable('auth_github'), + knex.schema.dropTable('auth_linkedin'), + knex.schema.dropTable('user_profile'), + knex.schema.dropTable('user') + ]); +}; diff --git a/modules/user/server-ts/package.json b/modules/user/server-ts/package.json new file mode 100644 index 0000000..e8dd2bb --- /dev/null +++ b/modules/user/server-ts/package.json @@ -0,0 +1,5 @@ +{ + "name": "@restapp/user-server-ts", + "version": "0.1.0", + "private": true +} \ No newline at end of file diff --git a/modules/user/server-ts/seeds/.eslintrc b/modules/user/server-ts/seeds/.eslintrc new file mode 100644 index 0000000..712a899 --- /dev/null +++ b/modules/user/server-ts/seeds/.eslintrc @@ -0,0 +1,5 @@ +{ + "rules": { + "import/prefer-default-export": 0 + } +} \ No newline at end of file diff --git a/modules/user/server-ts/seeds/002_user.js b/modules/user/server-ts/seeds/002_user.js new file mode 100644 index 0000000..7acf33c --- /dev/null +++ b/modules/user/server-ts/seeds/002_user.js @@ -0,0 +1,36 @@ +import bcrypt from 'bcryptjs'; +import { returnId, truncateTables } from '@restapp/database-server-ts'; + +export async function seed(knex, Promise) { + await truncateTables(knex, Promise, [ + 'user', + 'user_profile', + 'auth_certificate', + 'auth_facebook', + 'auth_github', + 'auth_linkedin' + ]); + + const id = await returnId(knex('user')).insert({ + username: 'admin', + email: 'admin@example.com', + password_hash: await bcrypt.hash('admin123', 12), + role: 'admin', + is_active: true + }); + + await returnId( + knex('auth_certificate').insert({ + serial: 'admin-123', + user_id: id[0] + }) + ); + + await returnId(knex('user')).insert({ + username: 'user', + email: 'user@example.com', + password_hash: await bcrypt.hash('user1234', 12), + role: 'user', + is_active: true + }); +} diff --git a/modules/user/server-ts/sql.ts b/modules/user/server-ts/sql.ts new file mode 100644 index 0000000..45c0e28 --- /dev/null +++ b/modules/user/server-ts/sql.ts @@ -0,0 +1,285 @@ +// Helpers +import { camelizeKeys, decamelizeKeys, decamelize } from 'humps'; +import { has } from 'lodash'; +import bcrypt from 'bcryptjs'; +import { knex, returnId } from '@restapp/database-server-ts'; + +export interface UserShape { + id: number; + username: string; + role: string; + isActive: boolean; + email: string; + passwordHash: string; + userId: number; +} + +interface OrderBy { + column: string; + order: string; +} + +interface Filter { + role: string; + isActive: boolean; + searchText: string; +} + +interface SocialInterface { + id: number; + displayName: string; + userId: number; +} + +export interface Profile { + firstName?: string; + lastName?: string; + id?: number; + username: string; + email: string; + role?: string; + isActive: boolean; +} + +const userColumns = ['u.id', 'u.username', 'u.role', 'u.is_active', 'u.email', 'up.first_name', 'up.last_name']; +const userColumnsWithSocial = [ + ...userColumns, + 'fa.fb_id', + 'fa.display_name AS fbDisplayName', + 'lna.ln_id', + 'lna.display_name AS lnDisplayName', + 'gha.gh_id', + 'gha.display_name AS ghDisplayName', + 'ga.google_id', + 'ga.display_name AS googleDisplayName' +]; +const userColumnsWithPassword = [...userColumns, 'u.password_hash']; + +class UserDAO { + public async getUsers(orderBy: OrderBy, filter: Filter) { + const queryBuilder = knex + .select(...userColumnsWithSocial) + .from('user AS u') + .leftJoin('user_profile AS up', 'up.user_id', 'u.id') + .leftJoin('auth_facebook AS fa', 'fa.user_id', 'u.id') + .leftJoin('auth_google AS ga', 'ga.user_id', 'u.id') + .leftJoin('auth_github AS gha', 'gha.user_id', 'u.id') + .leftJoin('auth_linkedin AS lna', 'lna.user_id', 'u.id'); + + // add order by + if (orderBy && orderBy.column) { + const column = orderBy.column; + let order = 'asc'; + if (orderBy.order) { + order = orderBy.order; + } + + queryBuilder.orderBy(decamelize(column), order); + } + + // add filter conditions + if (filter) { + if (has(filter, 'role') && filter.role !== '') { + queryBuilder.where(function() { + this.where('u.role', filter.role); + }); + } + + if (has(filter, 'isActive') && filter.isActive !== null) { + queryBuilder.where(function() { + this.where('u.is_active', filter.isActive); + }); + } + + if (has(filter, 'searchText') && filter.searchText !== '') { + queryBuilder.where(function() { + this.where(knex.raw('LOWER(??) LIKE LOWER(?)', ['username', `%${filter.searchText}%`])) + .orWhere(knex.raw('LOWER(??) LIKE LOWER(?)', ['email', `%${filter.searchText}%`])) + .orWhere(knex.raw('LOWER(??) LIKE LOWER(?)', ['first_name', `%${filter.searchText}%`])) + .orWhere(knex.raw('LOWER(??) LIKE LOWER(?)', ['last_name', `%${filter.searchText}%`])); + }); + } + } + + return camelizeKeys(await queryBuilder); + } + + public async getUser(id: number) { + return camelizeKeys( + await knex + .select(...userColumnsWithSocial) + .from('user AS u') + .leftJoin('user_profile AS up', 'up.user_id', 'u.id') + .leftJoin('auth_facebook AS fa', 'fa.user_id', 'u.id') + .leftJoin('auth_google AS ga', 'ga.user_id', 'u.id') + .leftJoin('auth_github AS gha', 'gha.user_id', 'u.id') + .leftJoin('auth_linkedin AS lna', 'lna.user_id', 'u.id') + .where('u.id', '=', id) + .first() + ); + } + + public async getUserWithPassword(id: number) { + return camelizeKeys( + await knex + .select(...userColumnsWithPassword) + .from('user AS u') + .where('u.id', '=', id) + .leftJoin('user_profile AS up', 'up.user_id', 'u.id') + .first() + ); + } + + public register({ username, email, role = 'user', isActive }: Profile, passwordHash?: string | false) { + return knex('user').insert(decamelizeKeys({ username, email, role, passwordHash, isActive })); + } + + public createFacebookAuth({ id, displayName, userId }: SocialInterface) { + return returnId(knex('auth_facebook')).insert({ fb_id: id, display_name: displayName, user_id: userId }); + } + + public createGithubAuth({ id, displayName, userId }: SocialInterface) { + return returnId(knex('auth_github')).insert({ gh_id: id, display_name: displayName, user_id: userId }); + } + + public createGoogleOAuth({ id, displayName, userId }: SocialInterface) { + return returnId(knex('auth_google')).insert({ google_id: id, display_name: displayName, user_id: userId }); + } + + public createLinkedInAuth({ id, displayName, userId }: SocialInterface) { + return returnId(knex('auth_linkedin')).insert({ ln_id: id, display_name: displayName, user_id: userId }); + } + + public editUser({ id, username, email, role, isActive }: Profile, passwordHash: string) { + const localAuthInput = passwordHash ? { email, passwordHash } : { email }; + return knex('user') + .update(decamelizeKeys({ username, role, isActive, ...localAuthInput })) + .where({ id }); + } + + public async isUserProfileExists(userId: number) { + return !!(await knex('user_profile') + .count('id as count') + .where(decamelizeKeys({ userId })) + .first()).count; + } + + public editUserProfile({ id, profile }: { id: number; profile: any }, isExists?: boolean) { + if (isExists) { + return knex('user_profile') + .update(decamelizeKeys(profile)) + .where({ user_id: id }); + } else { + return returnId(knex('user_profile')).insert({ ...decamelizeKeys(profile), user_id: id }); + } + } + + public deleteUser(id: number) { + return knex('user') + .where('id', '=', id) + .del(); + } + + public async updatePassword(id: number, newPassword: string) { + const passwordHash = await bcrypt.hash(newPassword, 12); + + return knex('user') + .update({ password_hash: passwordHash }) + .where({ id }); + } + + public updateActive(id: number, isActive: number) { + return knex('user') + .update({ is_active: isActive }) + .where({ id }); + } + + public async getUserByEmail(email: string) { + return camelizeKeys( + await knex + .select(...userColumnsWithPassword) + .from('user AS u') + .leftJoin('user_profile AS up', 'up.user_id', 'u.id') + .where({ email }) + .first() + ); + } + + public async getUserByFbIdOrEmail(id: number, email: string) { + return camelizeKeys( + await knex + .select(...userColumnsWithPassword, 'fa.fb_id') + .from('user AS u') + .leftJoin('auth_facebook AS fa', 'fa.user_id', 'u.id') + .leftJoin('user_profile AS up', 'up.user_id', 'u.id') + .where('fa.fb_id', '=', id) + .orWhere('u.email', '=', email) + .first() + ); + } + + public async getUserByLnInIdOrEmail(id: number, email: string = '') { + return camelizeKeys( + await knex + .select(...userColumnsWithPassword, 'lna.ln_id') + .from('user AS u') + .leftJoin('auth_linkedin AS lna', 'lna.user_id', 'u.id') + .leftJoin('user_profile AS up', 'up.user_id', 'u.id') + .where('lna.ln_id', '=', id) + .orWhere('u.email', '=', email) + .first() + ); + } + + public async getUserByGHIdOrEmail(id: number, email: string) { + return camelizeKeys( + await knex + .select(...userColumnsWithPassword, 'gha.gh_id') + .from('user AS u') + .leftJoin('auth_github AS gha', 'gha.user_id', 'u.id') + .leftJoin('user_profile AS up', 'up.user_id', 'u.id') + .where('gha.gh_id', '=', id) + .orWhere('u.email', '=', email) + .first() + ); + } + + public async getUserByGoogleIdOrEmail(id: number, email: string) { + return camelizeKeys( + await knex + .select(...userColumnsWithPassword, 'ga.google_id') + .from('user AS u') + .leftJoin('auth_google AS ga', 'ga.user_id', 'u.id') + .leftJoin('user_profile AS up', 'up.user_id', 'u.id') + .where('ga.google_id', '=', id) + .orWhere('u.email', '=', email) + .first() + ); + } + + public async getUserByUsername(username: string) { + return camelizeKeys( + await knex + .select(...userColumns) + .from('user AS u') + .where('u.username', '=', username) + .leftJoin('user_profile AS up', 'up.user_id', 'u.id') + .first() + ); + } + + public async getUserByUsernameOrEmail(usernameOrEmail: string) { + return camelizeKeys( + await knex + .select(...userColumnsWithPassword) + .from('user AS u') + .where('u.username', '=', usernameOrEmail) + .orWhere('u.email', '=', usernameOrEmail) + .leftJoin('user_profile AS up', 'up.user_id', 'u.id') + .first() + ); + } +} +const userDAO = new UserDAO(); + +export default userDAO; diff --git a/packages/server/package.json b/packages/server/package.json index 27755cd..ac3ed97 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -54,6 +54,7 @@ "@restapp/core-common": "^0.1.0", "@restapp/database-server-ts": "^0.1.0", "@restapp/testing-server-ts": "^0.1.0", + "@restapp/user-server-ts": "^0.1.0", "@restapp/welcome-server-ts": "^0.1.0", "@types/pdfmake": "^0.1.3", "bcryptjs": "^2.4.3", @@ -105,6 +106,7 @@ }, "devDependencies": { "@alienfast/i18next-loader": "^1.0.15", + "@types/bcryptjs": "^2.4.2", "@types/chai": "^4.1.4", "@types/chai-http": "^3.0.5", "@types/humps": "^1.1.2", @@ -141,4 +143,4 @@ "peerDependencies": { "@larix/zen": "^0.1.0" } -} +} \ No newline at end of file diff --git a/packages/server/src/modules.ts b/packages/server/src/modules.ts index 4de5266..fe5ba12 100644 --- a/packages/server/src/modules.ts +++ b/packages/server/src/modules.ts @@ -4,10 +4,11 @@ import i18n from '@restapp/i18n-server-ts'; import validation from '@restapp/validation-common-react'; import cookies from '@restapp/cookies-server-ts'; import mailer from '@restapp/mailer-server-ts'; +import user from '@restapp/user-server-ts'; import '@restapp/debug-server-ts'; import ServerModule from '@restapp/module-server-ts'; -const modules: ServerModule = new ServerModule(welcome, cookies, i18n, validation, mailer, core); +const modules: ServerModule = new ServerModule(welcome, cookies, i18n, validation, mailer, core, user); export default modules; diff --git a/yarn.lock b/yarn.lock index 8ec7a10..284fecd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1980,6 +1980,11 @@ resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd" integrity sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow== +"@types/bcryptjs@^2.4.2": + version "2.4.2" + resolved "https://registry.yarnpkg.com/@types/bcryptjs/-/bcryptjs-2.4.2.tgz#e3530eac9dd136bfdfb0e43df2c4c5ce1f77dfae" + integrity sha512-LiMQ6EOPob/4yUL66SZzu6Yh77cbzJFYll+ZfaPiPPFswtIlA/Fs1MzdKYA7JApHU49zQTbJGX3PDmCpIdDBRQ== + "@types/bluebird@*": version "3.5.26" resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.26.tgz#a38c438ae84fa02431d6892edf86e46edcbca291" @@ -9179,7 +9184,7 @@ immediate@^3.2.2: resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.2.3.tgz#d140fa8f614659bd6541233097ddaac25cdd991c" integrity sha1-0UD6j2FGWb1lQSMwl92qwlzdmRw= -immutability-helper@2.8.1, immutability-helper@^2.6.2: +immutability-helper@^2.6.2: version "2.8.1" resolved "https://registry.yarnpkg.com/immutability-helper/-/immutability-helper-2.8.1.tgz#3c5ec05fcd83676bfae7146f319595243ad904f4" integrity sha512-8AVB5EUpRBUdXqfe4cFsFECsOIZ9hX/Arl8B8S9/tmwpYv3UWvOsXUPOjkuZIMaVxfSWkxCzkng1rjmEoSWrxQ== @@ -10028,11 +10033,6 @@ items@2.x.x: resolved "https://registry.yarnpkg.com/items/-/items-2.1.2.tgz#0849354595805d586dac98e7e6e85556ea838558" integrity sha512-kezcEqgB97BGeZZYtX/MA8AG410ptURstvnz5RAgyFZ8wQFPMxHY8GpTq+/ZHKT3frSlIthUq7EvLt9xn3TvXg== -iterall@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.2.2.tgz#92d70deb8028e0c39ff3164fdbf4d8b088130cd7" - integrity sha512-yynBb1g+RFUPY64fTrFv7nsjRrENBQJaX2UL+2Szc9REFrSNm1rpSXHGzhmAy7a9uv3vlvgBlXnf9RqmPH1/DA== - jest-changed-files@^23.4.2: version "23.4.2" resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-23.4.2.tgz#1eed688370cd5eebafe4ae93d34bb3b64968fe83" @@ -18324,7 +18324,7 @@ watch@~0.18.0: exec-sh "^0.2.0" minimist "^1.2.0" -watchpack@^1.5.0, "watchpack@https://github.com/Globegitter/watchpack": +watchpack@^1.5.0: version "1.6.0" resolved "https://github.com/Globegitter/watchpack#d903e64a910ab068299f824b04a5e216a15672e9" dependencies: From 96a444c7f4ffd89991234d60d09619224faa7e46 Mon Sep 17 00:00:00 2001 From: Alexandr Belokur Date: Tue, 7 May 2019 10:30:55 +0300 Subject: [PATCH 012/104] Update createServerApp --- modules/core/server-ts/app.ts | 5 +++++ modules/core/server-ts/middleware/context.ts | 8 ++++++++ 2 files changed, 13 insertions(+) create mode 100644 modules/core/server-ts/middleware/context.ts diff --git a/modules/core/server-ts/app.ts b/modules/core/server-ts/app.ts index deddb35..dd85004 100644 --- a/modules/core/server-ts/app.ts +++ b/modules/core/server-ts/app.ts @@ -6,6 +6,7 @@ import ServerModule from '@restapp/module-server-ts'; import websiteMiddleware from './middleware/website'; import errorMiddleware from './middleware/error'; +import contextMiddleware from './middleware/context'; export const createServerApp = (modules: ServerModule) => { const app = express(); @@ -24,6 +25,10 @@ export const createServerApp = (modules: ServerModule) => { } if (!isApiExternal) { + app.use(contextMiddleware(modules)); + if (modules.apiRoutes) { + modules.apiRoutes.forEach(applyMiddleware => applyMiddleware(app, modules)); + } app.get('/api', (req, res, next) => res.json({ message: 'REST API: Success' })); } diff --git a/modules/core/server-ts/middleware/context.ts b/modules/core/server-ts/middleware/context.ts new file mode 100644 index 0000000..cdd1cd0 --- /dev/null +++ b/modules/core/server-ts/middleware/context.ts @@ -0,0 +1,8 @@ +import ServerModule from '@restapp/module-server-ts'; + +const contextMiddleware = ({ appContext }: ServerModule) => (req: any, res: any, next: () => void) => { + res.locals.appContext = appContext; + next(); +}; + +export default contextMiddleware; From 83185c704abfbc952d1e28a6453995815376191a Mon Sep 17 00:00:00 2001 From: Alexandr Belokur Date: Tue, 7 May 2019 11:03:18 +0300 Subject: [PATCH 013/104] Add emailTemplate --- modules/user/server-ts/emailTemplate.ts | 32 +++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 modules/user/server-ts/emailTemplate.ts diff --git a/modules/user/server-ts/emailTemplate.ts b/modules/user/server-ts/emailTemplate.ts new file mode 100644 index 0000000..8f4d9e2 --- /dev/null +++ b/modules/user/server-ts/emailTemplate.ts @@ -0,0 +1,32 @@ +import { UserShape } from './sql'; +import settings from '../../../settings'; + +const { app } = settings; + +const accountCreated = (url: string, user: UserShape) => + `

Hi, ${user.username}!

+

Welcome to ${app.name}. Please click the following link to confirm your email:

+

${url}

+

Below are your login information

+

Your email is: ${user.email}

`; + +const passwordUpdated = (url: string) => + `

Your account password has been updated.

+

To view or edit your account settings, please visit the “Profile” page at

+

${url}

`; + +const confirmEmail = (url: string, user: UserShape) => + `

Hi, ${user.username}!

+

Welcome to ${app.name}. Please click the following link to confirm your email:

+

${url}

+

Below are your login information

+

Your email is: ${user.email}

`; + +const passwordReset = (url: string) => `Please click this link to reset your password: ${url}`; + +export default { + accountCreated, + passwordUpdated, + confirmEmail, + passwordReset +}; From 05e59c74b805d4b4b4a1b30808020f8bd67a3960 Mon Sep 17 00:00:00 2001 From: Alexandr Belokur Date: Tue, 7 May 2019 11:09:47 +0300 Subject: [PATCH 014/104] Add password sub-module --- modules/user/server-ts/index.ts | 11 ++- .../user/server-ts/password/controllers.ts | 69 +++++++++++++++++++ modules/user/server-ts/password/index.ts | 20 ++++++ packages/server/package.json | 3 +- yarn.lock | 7 ++ 5 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 modules/user/server-ts/password/controllers.ts create mode 100644 modules/user/server-ts/password/index.ts diff --git a/modules/user/server-ts/index.ts b/modules/user/server-ts/index.ts index 20bcfdc..dca713e 100644 --- a/modules/user/server-ts/index.ts +++ b/modules/user/server-ts/index.ts @@ -1,3 +1,12 @@ import ServerModule from '@restapp/module-server-ts'; +import password from './password'; -export default new ServerModule(); +import resources from './locales'; + +export interface ValidationErrors { + username?: string; + email?: string; + password?: string; +} + +export default new ServerModule(password, { localization: [{ ns: 'user', resources }] }); diff --git a/modules/user/server-ts/password/controllers.ts b/modules/user/server-ts/password/controllers.ts new file mode 100644 index 0000000..3981d5c --- /dev/null +++ b/modules/user/server-ts/password/controllers.ts @@ -0,0 +1,69 @@ +import { UserShape } from './../sql'; +import { pick, isEmpty } from 'lodash'; +import jwt from 'jsonwebtoken'; +import { log } from '@restapp/core-common'; +import { mailer } from '@restapp/mailer-server-ts'; + +import userDAO from '../sql'; +import settings from '../../../../settings'; +import { ValidationErrors } from '../'; +import emailTemplate from '../emailTemplate'; +import { createPasswordHash } from '.'; + +const { + auth: { password, secret }, + app +} = settings; + +export const register = async ({ body, t }: any, res: any) => { + const errors: ValidationErrors = {}; + const userExists = await userDAO.getUserByUsername(body.username); + if (userExists) { + errors.username = t('user:auth.password.usernameIsExisted'); + } + + const emailExists = (await userDAO.getUserByEmail(body.email)) as UserShape; + if (emailExists) { + errors.email = t('user:auth.password.emailIsExisted'); + } + + if (!isEmpty(errors)) { + return res.send({ + error: { + message: 'Failed reset password', + errors + } + }); + } + + let userId = 0; + if (!emailExists) { + const passwordHash = await createPasswordHash(body.password); + const isActive = !password.requireEmailConfirmation; + [userId] = await userDAO.register({ ...body, isActive }, passwordHash); + + // if user has previously logged with facebook auth + } else { + await userDAO.updatePassword(emailExists.userId, body.password); + userId = emailExists.userId; + } + + const user = (await userDAO.getUser(userId)) as UserShape; + + if (mailer && password.requireEmailConfirmation && !emailExists) { + // async email + jwt.sign({ identity: pick(user, 'id') }, secret, { expiresIn: '1d' }, (err, emailToken) => { + const encodedToken = Buffer.from(emailToken).toString('base64'); + const url = `${__WEBSITE_URL__}/confirmation/${encodedToken}`; + mailer.sendMail({ + from: `${app.name} <${process.env.EMAIL_USER}>`, + to: user.email, + subject: 'Confirm Email', + html: emailTemplate.confirmEmail(url, user) + }); + log.info(`Sent registration confirmation email to: ${user.email}`); + }); + } + + res.json(user); +}; diff --git a/modules/user/server-ts/password/index.ts b/modules/user/server-ts/password/index.ts new file mode 100644 index 0000000..cb9ac42 --- /dev/null +++ b/modules/user/server-ts/password/index.ts @@ -0,0 +1,20 @@ +import bcrypt from 'bcryptjs'; + +import ServerModule, { RestMethod } from '@restapp/module-server-ts'; + +import { register } from './controllers'; +import settings from '../../../../settings'; + +export const createPasswordHash = (pswd: string) => bcrypt.hash(pswd, 12); + +export default (settings.auth.password.enabled + ? new ServerModule({ + apiRouteParams: [ + { + method: RestMethod.POST, + route: 'register', + controller: register + } + ] + }) + : undefined); diff --git a/packages/server/package.json b/packages/server/package.json index ac3ed97..dcd8c09 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -112,6 +112,7 @@ "@types/humps": "^1.1.2", "@types/i18next": "^8.4.4", "@types/i18next-express-middleware": "^0.0.33", + "@types/jsonwebtoken": "^8.3.2", "@types/knex": "^0.14.20", "@types/mkdirp": "^0.5.2", "@types/nodemailer": "^4.6.2", @@ -143,4 +144,4 @@ "peerDependencies": { "@larix/zen": "^0.1.0" } -} \ No newline at end of file +} diff --git a/yarn.lock b/yarn.lock index 284fecd..541e8ac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2142,6 +2142,13 @@ "@types/tough-cookie" "*" parse5 "^4.0.0" +"@types/jsonwebtoken@^8.3.2": + version "8.3.2" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-8.3.2.tgz#e3d5245197152346fae7ee87d5541aa5a92d0362" + integrity sha512-Mkjljd9DTpkPlrmGfTJvcP4aBU7yO2QmW7wNVhV4/6AEUxYoacqU7FJU/N0yFEHTsIrE4da3rUrjrR5ejicFmA== + dependencies: + "@types/node" "*" + "@types/knex@^0.14.20": version "0.14.26" resolved "https://registry.yarnpkg.com/@types/knex/-/knex-0.14.26.tgz#18c1b5d5a7e44bb0d394209f7e0e965b2987ce57" From 90c5115c9db5579f7d10342df1c1a3e62e9bd438 Mon Sep 17 00:00:00 2001 From: Alexandr Belokur Date: Tue, 7 May 2019 11:10:00 +0300 Subject: [PATCH 015/104] Add bodyParser for app --- modules/core/server-ts/app.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/core/server-ts/app.ts b/modules/core/server-ts/app.ts index dd85004..fdaaad8 100644 --- a/modules/core/server-ts/app.ts +++ b/modules/core/server-ts/app.ts @@ -1,4 +1,5 @@ import express from 'express'; +import bodyParser from 'body-parser'; import path from 'path'; import { isApiExternal } from '@restapp/core-common'; @@ -26,6 +27,9 @@ export const createServerApp = (modules: ServerModule) => { if (!isApiExternal) { app.use(contextMiddleware(modules)); + app.use(bodyParser.urlencoded({ extended: false })); + app.use(bodyParser.json()); + if (modules.apiRoutes) { modules.apiRoutes.forEach(applyMiddleware => applyMiddleware(app, modules)); } From d2f2a5ffb9196a34f1a20a0ac5416cef20e73eb3 Mon Sep 17 00:00:00 2001 From: Ivan Isakov Date: Tue, 7 May 2019 11:16:07 +0300 Subject: [PATCH 016/104] Change form error handler --- .../containers/Register.native.tsx | 19 +++++++++--------- .../user/client-react/containers/Register.tsx | 20 +++++++++---------- modules/user/client-react/index.native.tsx | 2 +- 3 files changed, 20 insertions(+), 21 deletions(-) diff --git a/modules/user/client-react/containers/Register.native.tsx b/modules/user/client-react/containers/Register.native.tsx index 80eb7b4..666ab79 100644 --- a/modules/user/client-react/containers/Register.native.tsx +++ b/modules/user/client-react/containers/Register.native.tsx @@ -10,7 +10,7 @@ import { REGISTER } from '../actions'; import { CommonProps, RegisterSubmitProps } from '../index.native'; interface RegisterProps extends CommonProps { - register: (values: RegisterSubmitProps) => void; + register: (values: RegisterSubmitProps) => any; } interface RegisterState { @@ -25,15 +25,14 @@ class Register extends React.Component { public onSubmit = async (values: RegisterSubmitProps) => { const { t, register, navigation } = this.props; - try { - await register(values); - if (!settings.auth.password.requireEmailConfirmation) { - navigation.goBack(); - } else { - this.setState({ isRegistered: true }); - } - } catch (e) { - throw new FormError(t('reg.errorMsg'), e); + const { data } = await register(values); + if (data.error) { + throw new FormError(t('reg.errorMsg'), data.error); + } + if (!settings.auth.password.requireEmailConfirmation) { + navigation.goBack(); + } else { + this.setState({ isRegistered: true }); } }; diff --git a/modules/user/client-react/containers/Register.tsx b/modules/user/client-react/containers/Register.tsx index b514064..0a513bf 100644 --- a/modules/user/client-react/containers/Register.tsx +++ b/modules/user/client-react/containers/Register.tsx @@ -10,7 +10,7 @@ import { REGISTER } from '../actions'; import settings from '../../../../settings'; interface RegisterProps extends CommonProps { - register: (values: RegisterSubmitProps) => void; + register: (values: RegisterSubmitProps) => any; } const Register: React.FunctionComponent = props => { @@ -19,15 +19,15 @@ const Register: React.FunctionComponent = props => { const [isRegistered, setIsRegistered] = React.useState(false); const onSubmit = async (values: RegisterSubmitProps) => { - try { - await register(values); - if (!settings.auth.password.requireEmailConfirmation) { - history.push('/'); - } else { - setIsRegistered(true); - } - } catch (e) { - throw new FormError(t('reg.errorMsg'), e); + const { data } = await register(values); + if (data.error) { + throw new FormError(t('reg.errorMsg'), data.error); + } + + if (!settings.auth.password.requireEmailConfirmation) { + history.push('/'); + } else { + setIsRegistered(true); } }; diff --git a/modules/user/client-react/index.native.tsx b/modules/user/client-react/index.native.tsx index 05c575a..d9eb2e0 100644 --- a/modules/user/client-react/index.native.tsx +++ b/modules/user/client-react/index.native.tsx @@ -7,7 +7,7 @@ import { NavigationParams } from 'react-navigation'; import { translate, TranslateFunction } from '@restapp/i18n-client-react'; -import { HeaderTitle, IconButton } from '@restapp/look-client-react-native'; +import { HeaderTitle } from '@restapp/look-client-react-native'; import ClientModule from '@restapp/module-client-react-native'; import { FormikErrors } from 'formik'; import resources from './locales'; From 79cea349f54eaf710a50bc4f5036070a3b778390 Mon Sep 17 00:00:00 2001 From: Alexandr Belokur Date: Tue, 7 May 2019 14:16:39 +0300 Subject: [PATCH 017/104] Add status for errors response in register --- modules/user/server-ts/password/controllers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/user/server-ts/password/controllers.ts b/modules/user/server-ts/password/controllers.ts index 3981d5c..59c5106 100644 --- a/modules/user/server-ts/password/controllers.ts +++ b/modules/user/server-ts/password/controllers.ts @@ -28,7 +28,7 @@ export const register = async ({ body, t }: any, res: any) => { } if (!isEmpty(errors)) { - return res.send({ + return res.status(422).send({ error: { message: 'Failed reset password', errors From 1cd7ed295512e0bb0f51e57b81c5711700942c63 Mon Sep 17 00:00:00 2001 From: Alexandr Belokur Date: Tue, 7 May 2019 14:17:20 +0300 Subject: [PATCH 018/104] Fix typo in migrations --- modules/user/server-ts/{migration => migrations}/002_user.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename modules/user/server-ts/{migration => migrations}/002_user.js (100%) diff --git a/modules/user/server-ts/migration/002_user.js b/modules/user/server-ts/migrations/002_user.js similarity index 100% rename from modules/user/server-ts/migration/002_user.js rename to modules/user/server-ts/migrations/002_user.js From 65e6ab8eebdc142db27120f814b9fc721955aded Mon Sep 17 00:00:00 2001 From: Ivan Isakov Date: Tue, 7 May 2019 14:41:13 +0300 Subject: [PATCH 019/104] Fix redux middleware --- modules/core/common/createReduxStore.ts | 21 ++++++++++--------- modules/user/client-react/actions/register.ts | 10 ++++++--- .../containers/Register.native.tsx | 14 +++++-------- .../user/client-react/containers/Register.tsx | 16 ++++++-------- modules/user/client-react/reducers/index.ts | 13 ++++++++++-- .../{migration => migrations}/002_user.js | 0 yarn.lock | 9 ++++++-- 7 files changed, 47 insertions(+), 36 deletions(-) rename modules/user/server-ts/{migration => migrations}/002_user.js (100%) diff --git a/modules/core/common/createReduxStore.ts b/modules/core/common/createReduxStore.ts index 63a7586..edd869c 100644 --- a/modules/core/common/createReduxStore.ts +++ b/modules/core/common/createReduxStore.ts @@ -9,35 +9,36 @@ export const getStoreReducer = (reducers: any) => }); const requestMiddleware: Middleware = _state => next => action => { - const { type, request, ...rest } = action; - if (!request) { - return next(action); - } + const { types, callAPI, ...rest } = action; - if (!type) { - request(); + if (!types) { return next(action); } - const [REQUEST, SUCCESS, FAIL] = type; + const [REQUEST, SUCCESS, FAIL] = types; + next({ type: REQUEST, ...rest }); - (async () => { + const handleCallApi = async () => { try { - const { data } = await request(); + const { data } = await callAPI(); next({ type: SUCCESS, payload: data, ...rest }); + return data; } catch (error) { next({ type: FAIL, payload: error, ...rest }); + return error; } - })(); + }; + + return handleCallApi(); }; const createReduxStore = (reducers: Reducer, initialState: DeepPartial, routerMiddleware?: Middleware): Store => { diff --git a/modules/user/client-react/actions/register.ts b/modules/user/client-react/actions/register.ts index 7fd53e1..9a3ee6d 100644 --- a/modules/user/client-react/actions/register.ts +++ b/modules/user/client-react/actions/register.ts @@ -1,6 +1,10 @@ import axios from 'axios'; import { RegisterSubmitProps } from '..'; +import { ActionType } from '../reducers'; -const REGISTER = async (value: RegisterSubmitProps) => axios.post(`${__API_URL__}/register`, { ...value }); - -export default REGISTER; +export default function REGISTER(value: RegisterSubmitProps) { + return { + types: [null, ActionType.REGISTER, null], + callAPI: async () => axios.post(`${__API_URL__}/register`, { ...value }) + }; +} diff --git a/modules/user/client-react/containers/Register.native.tsx b/modules/user/client-react/containers/Register.native.tsx index 666ab79..136d277 100644 --- a/modules/user/client-react/containers/Register.native.tsx +++ b/modules/user/client-react/containers/Register.native.tsx @@ -25,10 +25,12 @@ class Register extends React.Component { public onSubmit = async (values: RegisterSubmitProps) => { const { t, register, navigation } = this.props; - const { data } = await register(values); + const data = await register(values); + if (data.error) { throw new FormError(t('reg.errorMsg'), data.error); } + if (!settings.auth.password.requireEmailConfirmation) { navigation.goBack(); } else { @@ -53,14 +55,8 @@ class Register extends React.Component { } const withConnect = connect( - _state => ({}), - dispatch => ({ - register: (value: RegisterSubmitProps) => - dispatch({ - type: null, - request: () => REGISTER(value) - }) - }) + null, + { register: REGISTER } ); export default compose( diff --git a/modules/user/client-react/containers/Register.tsx b/modules/user/client-react/containers/Register.tsx index 0a513bf..8154d75 100644 --- a/modules/user/client-react/containers/Register.tsx +++ b/modules/user/client-react/containers/Register.tsx @@ -11,6 +11,7 @@ import settings from '../../../../settings'; interface RegisterProps extends CommonProps { register: (values: RegisterSubmitProps) => any; + data: any; } const Register: React.FunctionComponent = props => { @@ -19,9 +20,10 @@ const Register: React.FunctionComponent = props => { const [isRegistered, setIsRegistered] = React.useState(false); const onSubmit = async (values: RegisterSubmitProps) => { - const { data } = await register(values); + const data = await register(values); + if (data.error) { - throw new FormError(t('reg.errorMsg'), data.error); + throw new FormError(t('reg.errorMsg'), data.error.errors); } if (!settings.auth.password.requireEmailConfirmation) { @@ -35,12 +37,6 @@ const Register: React.FunctionComponent = props => { }; export default connect( - _state => ({}), - dispatch => ({ - register: (value: RegisterSubmitProps) => - dispatch({ - type: null, - request: () => REGISTER(value) - }) - }) + null, + { register: REGISTER } )(translate('user')(Register)); diff --git a/modules/user/client-react/reducers/index.ts b/modules/user/client-react/reducers/index.ts index 11df66c..87478dd 100644 --- a/modules/user/client-react/reducers/index.ts +++ b/modules/user/client-react/reducers/index.ts @@ -3,12 +3,14 @@ import { User } from '..'; export enum ActionType { SET_CURRENT_USER = 'SET_CURRENT_USER', CLEAR_CURRENT_USER = 'CLEAR_CURRENT_USER', - SET_LOADING = 'SET_LOADING' + SET_LOADING = 'SET_LOADING', + REGISTER = 'REGISTER' } export interface UserModuleState { currentUser: User; loading: boolean; + register: any; } export interface UserModuleActionProps { @@ -20,11 +22,18 @@ export interface UserModuleActionProps { const defaultState: UserModuleState = { currentUser: null, - loading: false + loading: false, + register: null }; export default function(state = defaultState, action: UserModuleActionProps) { switch (action.type) { + case ActionType.REGISTER: + return { + ...state, + register: action.payload + }; + case ActionType.SET_LOADING: return { ...state, diff --git a/modules/user/server-ts/migration/002_user.js b/modules/user/server-ts/migrations/002_user.js similarity index 100% rename from modules/user/server-ts/migration/002_user.js rename to modules/user/server-ts/migrations/002_user.js diff --git a/yarn.lock b/yarn.lock index 541e8ac..695d965 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9191,7 +9191,7 @@ immediate@^3.2.2: resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.2.3.tgz#d140fa8f614659bd6541233097ddaac25cdd991c" integrity sha1-0UD6j2FGWb1lQSMwl92qwlzdmRw= -immutability-helper@^2.6.2: +immutability-helper@2.8.1, immutability-helper@^2.6.2: version "2.8.1" resolved "https://registry.yarnpkg.com/immutability-helper/-/immutability-helper-2.8.1.tgz#3c5ec05fcd83676bfae7146f319595243ad904f4" integrity sha512-8AVB5EUpRBUdXqfe4cFsFECsOIZ9hX/Arl8B8S9/tmwpYv3UWvOsXUPOjkuZIMaVxfSWkxCzkng1rjmEoSWrxQ== @@ -10040,6 +10040,11 @@ items@2.x.x: resolved "https://registry.yarnpkg.com/items/-/items-2.1.2.tgz#0849354595805d586dac98e7e6e85556ea838558" integrity sha512-kezcEqgB97BGeZZYtX/MA8AG410ptURstvnz5RAgyFZ8wQFPMxHY8GpTq+/ZHKT3frSlIthUq7EvLt9xn3TvXg== +iterall@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.2.2.tgz#92d70deb8028e0c39ff3164fdbf4d8b088130cd7" + integrity sha512-yynBb1g+RFUPY64fTrFv7nsjRrENBQJaX2UL+2Szc9REFrSNm1rpSXHGzhmAy7a9uv3vlvgBlXnf9RqmPH1/DA== + jest-changed-files@^23.4.2: version "23.4.2" resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-23.4.2.tgz#1eed688370cd5eebafe4ae93d34bb3b64968fe83" @@ -18331,7 +18336,7 @@ watch@~0.18.0: exec-sh "^0.2.0" minimist "^1.2.0" -watchpack@^1.5.0: +watchpack@^1.5.0, "watchpack@https://github.com/Globegitter/watchpack": version "1.6.0" resolved "https://github.com/Globegitter/watchpack#d903e64a910ab068299f824b04a5e216a15672e9" dependencies: From be7ded238a273ef1af61c0a842569b55953cfab3 Mon Sep 17 00:00:00 2001 From: Alexandr Belokur Date: Tue, 7 May 2019 15:41:53 +0300 Subject: [PATCH 020/104] Add auth module --- .../server-ts/access/AccessModule.ts | 31 ++++++++ .../authentication/server-ts/access/index.ts | 20 +++++ .../server-ts/access/jwt/controllers.ts | 41 ++++++++++ .../server-ts/access/jwt/createTokens.ts | 20 +++++ .../server-ts/access/jwt/index.ts | 74 +++++++++++++++++++ .../server-ts/access/session/controllers.ts | 5 ++ .../server-ts/access/session/index.ts | 69 +++++++++++++++++ modules/authentication/server-ts/index.ts | 7 ++ .../server-ts/locales/en/translations.json | 7 ++ .../authentication/server-ts/locales/index.ts | 5 ++ .../server-ts/locales/ru/translations.json | 7 ++ modules/authentication/server-ts/package.json | 8 ++ .../server-ts/social/AuthModule.ts | 13 ++++ .../server-ts/social/facebook/controllers.ts | 15 ++++ .../server-ts/social/facebook/index.ts | 51 +++++++++++++ .../server-ts/social/github/controllers.ts | 15 ++++ .../server-ts/social/github/index.ts | 51 +++++++++++++ .../server-ts/social/google/controllers.ts | 25 +++++++ .../server-ts/social/google/index.ts | 53 +++++++++++++ .../authentication/server-ts/social/index.ts | 7 ++ .../server-ts/social/linkedIn/controllers.ts | 15 ++++ .../server-ts/social/linkedIn/index.ts | 51 +++++++++++++ 22 files changed, 590 insertions(+) create mode 100644 modules/authentication/server-ts/access/AccessModule.ts create mode 100644 modules/authentication/server-ts/access/index.ts create mode 100644 modules/authentication/server-ts/access/jwt/controllers.ts create mode 100644 modules/authentication/server-ts/access/jwt/createTokens.ts create mode 100644 modules/authentication/server-ts/access/jwt/index.ts create mode 100644 modules/authentication/server-ts/access/session/controllers.ts create mode 100644 modules/authentication/server-ts/access/session/index.ts create mode 100644 modules/authentication/server-ts/index.ts create mode 100644 modules/authentication/server-ts/locales/en/translations.json create mode 100644 modules/authentication/server-ts/locales/index.ts create mode 100644 modules/authentication/server-ts/locales/ru/translations.json create mode 100644 modules/authentication/server-ts/package.json create mode 100644 modules/authentication/server-ts/social/AuthModule.ts create mode 100644 modules/authentication/server-ts/social/facebook/controllers.ts create mode 100644 modules/authentication/server-ts/social/facebook/index.ts create mode 100644 modules/authentication/server-ts/social/github/controllers.ts create mode 100644 modules/authentication/server-ts/social/github/index.ts create mode 100644 modules/authentication/server-ts/social/google/controllers.ts create mode 100644 modules/authentication/server-ts/social/google/index.ts create mode 100644 modules/authentication/server-ts/social/index.ts create mode 100644 modules/authentication/server-ts/social/linkedIn/controllers.ts create mode 100644 modules/authentication/server-ts/social/linkedIn/index.ts diff --git a/modules/authentication/server-ts/access/AccessModule.ts b/modules/authentication/server-ts/access/AccessModule.ts new file mode 100644 index 0000000..1a01662 --- /dev/null +++ b/modules/authentication/server-ts/access/AccessModule.ts @@ -0,0 +1,31 @@ +import { UserShape } from './index'; +import { Request } from 'express'; +import { merge } from 'lodash'; +import ServerModule, { ServerModuleShape } from '@restapp/module-server-ts'; + +interface AccessModuleShape extends ServerModuleShape { + grant?: Array<(identity: UserShape, req: Request, passwordHash: string) => { [key: string]: any } | void>; + // grant?: (user: any) => Promise<[string, string]>; +} + +interface AccessModule extends AccessModuleShape {} + +type GrantAccessFunc = (identity: UserShape, req: Request, passwordHash: string) => Promise; + +class AccessModule extends ServerModule { + constructor(...modules: AccessModuleShape[]) { + super(...modules); + } + + get grantAccess(): GrantAccessFunc { + return async (identity: UserShape, req: Request, passwordHash: string) => { + let result = {}; + for (const grant of this.grant) { + result = merge(result, await grant(identity, req, passwordHash)); + } + return result; + }; + } +} + +export default AccessModule; diff --git a/modules/authentication/server-ts/access/index.ts b/modules/authentication/server-ts/access/index.ts new file mode 100644 index 0000000..e0b9b14 --- /dev/null +++ b/modules/authentication/server-ts/access/index.ts @@ -0,0 +1,20 @@ +import jwt from './jwt'; +import session from './session'; +import resources from '../locales'; + +import AccessModule from './AccessModule'; + +export interface UserShape { + id: number; + username: string; + role: string; + isActive: boolean; + email: string; + passwordHash: string; +} + +// Try to grant access via sessions first, and if that fails, then try using JWT +// This way if both JWT and sessions enabled UI won't have to refresh access tokens +export default new AccessModule(session, jwt, { + localization: [{ ns: 'auth', resources }] +}); diff --git a/modules/authentication/server-ts/access/jwt/controllers.ts b/modules/authentication/server-ts/access/jwt/controllers.ts new file mode 100644 index 0000000..ed769a5 --- /dev/null +++ b/modules/authentication/server-ts/access/jwt/controllers.ts @@ -0,0 +1,41 @@ +import jwt from 'jsonwebtoken'; + +import settings from '../../../../../settings'; +import createTokens from './createTokens'; + +export const refreshTokens = async (req: any, res: any) => { + const { + body: { refreshToken: inputRefreshToken }, + t + } = req; + const { + locals: { + appContext: { + user: { getHash, getIdentity } + } + } + } = res; + const decodedToken = jwt.decode(inputRefreshToken) as { [key: string]: any }; + const isValidToken = decodedToken && decodedToken.id; + + if (!isValidToken) { + res.send(t('auth:invalidRefresh')); + } + + const identity = await getIdentity(decodedToken.id); + const hash = getHash ? await getHash(decodedToken.id) : ''; + const refreshSecret = settings.auth.secret + hash; + + try { + jwt.verify(inputRefreshToken, refreshSecret); + } catch (err) { + res.send(err); + } + + const [accessToken, refreshToken] = await createTokens(identity, settings.auth.secret, refreshSecret, req.t); + + res.json({ + accessToken, + refreshToken + }); +}; diff --git a/modules/authentication/server-ts/access/jwt/createTokens.ts b/modules/authentication/server-ts/access/jwt/createTokens.ts new file mode 100644 index 0000000..604d631 --- /dev/null +++ b/modules/authentication/server-ts/access/jwt/createTokens.ts @@ -0,0 +1,20 @@ +import jwt from 'jsonwebtoken'; + +import { UserShape } from './../index'; +import settings from '../../../../../settings'; + +const { + jwt: { tokenExpiresIn, refreshTokenExpiresIn } +} = settings.auth; + +const createTokens = async (identity: UserShape, secret: string, refreshSecret: string, t: any) => { + if (!identity.id) { + throw new Error(t('auth:identityWithoutId')); + } + const createToken = jwt.sign({ identity }, secret, { expiresIn: tokenExpiresIn }); + const createRefreshToken = jwt.sign({ id: identity.id }, refreshSecret, { expiresIn: refreshTokenExpiresIn }); + + return Promise.all([createToken, createRefreshToken]); +}; + +export default createTokens; diff --git a/modules/authentication/server-ts/access/jwt/index.ts b/modules/authentication/server-ts/access/jwt/index.ts new file mode 100644 index 0000000..23f4d74 --- /dev/null +++ b/modules/authentication/server-ts/access/jwt/index.ts @@ -0,0 +1,74 @@ +import { Express } from 'express'; +import { Strategy as LocalStratery } from 'passport-local'; +import bodyParser from 'body-parser'; +import passport from 'passport'; +import { Strategy as JWTStrategy, ExtractJwt } from 'passport-jwt'; + +import { RestMethod } from '@restapp/module-server-ts'; +import settings from '../../../../../settings'; +import AccessModule from '../AccessModule'; +import { refreshTokens } from './controllers'; +import createTokens from './createTokens'; + +const { + auth: { secret } +} = settings; + +const beforeware = (app: Express) => { + app.use(bodyParser.urlencoded({ extended: false })); + app.use(bodyParser.json()); + app.use(passport.initialize()); +}; + +const accessMiddleware = passport.authenticate('jwt', { session: false }); + +const grant = async (identity: any, req: any, passwordHash: string = '') => { + const refreshSecret = settings.auth.secret + passwordHash; + const [accessToken, refreshToken] = await createTokens(identity, settings.auth.secret, refreshSecret, req.t); + + return { + accessToken, + refreshToken + }; +}; + +const onAppCreate = ({ appContext }: AccessModule) => { + passport.use( + new LocalStratery(async (username: string, password: string, done: any) => { + const { identity, message } = await appContext.user.validateLogin(username, password); + + if (message) { + return done(null, false, { message }); + } + return done(null, identity); + }) + ); + + passport.use( + new JWTStrategy( + { + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + secretOrKey: secret + }, + (jwtPayload: any, cb: any) => { + return cb(null, jwtPayload.identity); + } + ) + ); +}; + +export default (settings.auth.jwt.enabled + ? new AccessModule({ + beforeware: [beforeware], + onAppCreate: [onAppCreate], + grant: [grant], + apiRouteParams: [ + { + method: RestMethod.POST, + route: 'refreshToken', + controller: refreshTokens + } + ], + accessMiddleware + }) + : undefined); diff --git a/modules/authentication/server-ts/access/session/controllers.ts b/modules/authentication/server-ts/access/session/controllers.ts new file mode 100644 index 0000000..43205f6 --- /dev/null +++ b/modules/authentication/server-ts/access/session/controllers.ts @@ -0,0 +1,5 @@ +import { Request, Response } from 'express'; + +export const logout = (req: Request, res: Response) => { + req.logout(); +}; diff --git a/modules/authentication/server-ts/access/session/index.ts b/modules/authentication/server-ts/access/session/index.ts new file mode 100644 index 0000000..b73eac3 --- /dev/null +++ b/modules/authentication/server-ts/access/session/index.ts @@ -0,0 +1,69 @@ +import { Express, Request, Response } from 'express'; +import { Strategy } from 'passport-local'; +import bodyParser from 'body-parser'; +import session from 'express-session'; +import passport from 'passport'; + +import { RestMethod } from '@restapp/module-server-ts'; + +import settings from '../../../../../settings'; +import AccessModule from '../AccessModule'; +import { logout } from './controllers'; + +const FileStore = require('session-file-store')(session); + +const beforeware = (app: Express) => { + app.use(bodyParser.urlencoded({ extended: false })); + app.use(bodyParser.json()); + app.use( + session({ + secret: 'secret', + store: __DEV__ ? new FileStore() : null, + cookie: { maxAge: 60000 }, + resave: false, + saveUninitialized: false + }) + ); + app.use(passport.initialize()); + app.use(passport.session()); +}; + +const accessMiddleware = (req: Request, res: Response, next: any) => + req.isAuthenticated() ? next() : res.send('unauthorized'); + +const onAppCreate = ({ appContext }: AccessModule) => { + passport.serializeUser((identity: { id: number }, cb) => { + cb(null, identity.id); + }); + + passport.deserializeUser(async (id, cb) => { + const identity = await appContext.user.getIdentity(id); + return cb(null, identity); + }); + + passport.use( + new Strategy(async (username: string, password: string, done: any) => { + const { identity, message } = await appContext.user.validateLogin(username, password); + + if (message) { + return done(null, false, { message }); + } + return done(null, identity); + }) + ); +}; + +export default (settings.auth.session.enabled + ? new AccessModule({ + beforeware: [beforeware], + onAppCreate: [onAppCreate], + accessMiddleware, + apiRouteParams: [ + { + method: RestMethod.POST, + route: 'logout', + controller: logout + } + ] + }) + : undefined); diff --git a/modules/authentication/server-ts/index.ts b/modules/authentication/server-ts/index.ts new file mode 100644 index 0000000..29e12d5 --- /dev/null +++ b/modules/authentication/server-ts/index.ts @@ -0,0 +1,7 @@ +import ServerModule from '@restapp/module-server-ts'; +import AuthModule from './social/AuthModule'; +import access from './access'; +import social from './social'; + +export default new ServerModule(access, social); +export { access, AuthModule }; diff --git a/modules/authentication/server-ts/locales/en/translations.json b/modules/authentication/server-ts/locales/en/translations.json new file mode 100644 index 0000000..f1dd1a7 --- /dev/null +++ b/modules/authentication/server-ts/locales/en/translations.json @@ -0,0 +1,7 @@ +{ + "getIdentify": "Can not find 'getIdentity' method. Please, add this method to the context.", + "invalidCsrf": "CSRF token validation failed", + "identityWithoutId": "Identity must have 'id' method.", + "invalidRefresh": "Refresh token validation failed", + "social": "Cannot find social property on the context. Please, add this property to the context." +} \ No newline at end of file diff --git a/modules/authentication/server-ts/locales/index.ts b/modules/authentication/server-ts/locales/index.ts new file mode 100644 index 0000000..d9fb121 --- /dev/null +++ b/modules/authentication/server-ts/locales/index.ts @@ -0,0 +1,5 @@ +/* + * The index.js can be empty, it's just needed to point the loader to the root directory of the locales. + * https://github.com/alienfast/i18next-loader#option-2-use-with-import-syntax + */ +export default {}; diff --git a/modules/authentication/server-ts/locales/ru/translations.json b/modules/authentication/server-ts/locales/ru/translations.json new file mode 100644 index 0000000..d64617c --- /dev/null +++ b/modules/authentication/server-ts/locales/ru/translations.json @@ -0,0 +1,7 @@ +{ + "getIdentify": "Не доступен 'getIdentity' метод. Пожалуйста, добавьте метод в context.", + "invalidCsrf": "CSRF ошибка проверки токена", + "identityWithoutId": "Identity дожен иметь 'id'", + "invalidRefresh": "Refresh token не прошел проверку", + "social": "Не доступно свойство social в context. Пожалуйста, добавьте свойство в context." +} \ No newline at end of file diff --git a/modules/authentication/server-ts/package.json b/modules/authentication/server-ts/package.json new file mode 100644 index 0000000..110f74a --- /dev/null +++ b/modules/authentication/server-ts/package.json @@ -0,0 +1,8 @@ +{ + "name": "@restapp/authentication-server-ts", + "version": "0.1.0", + "private": true, + "dependencies": { + "@restapp/module-server-ts": "^0.1.0" + } +} diff --git a/modules/authentication/server-ts/social/AuthModule.ts b/modules/authentication/server-ts/social/AuthModule.ts new file mode 100644 index 0000000..4cf31cc --- /dev/null +++ b/modules/authentication/server-ts/social/AuthModule.ts @@ -0,0 +1,13 @@ +import ServerModule, { ServerModuleShape } from '@restapp/module-server-ts'; + +interface AuthModuleShape extends ServerModuleShape {} + +interface AuthModule extends AuthModuleShape {} + +class AuthModule extends ServerModule { + constructor(...modules: AuthModuleShape[]) { + super(...modules); + } +} + +export default AuthModule; diff --git a/modules/authentication/server-ts/social/facebook/controllers.ts b/modules/authentication/server-ts/social/facebook/controllers.ts new file mode 100644 index 0000000..37ee72c --- /dev/null +++ b/modules/authentication/server-ts/social/facebook/controllers.ts @@ -0,0 +1,15 @@ +import passport from 'passport'; + +export const auth = (req: any, res: any) => { + passport.authenticate('facebook', { state: req.query.expoUrl })(req, res); +}; + +export const onAuthenticationSuccess = (req: any, res: any) => { + const { + locals: { appContext } + } = res; + const { t } = req; + appContext.social + ? appContext.social.facebook.onAuthenticationSuccess(req, res) + : res.status(500).send(t('auth:social')); +}; diff --git a/modules/authentication/server-ts/social/facebook/index.ts b/modules/authentication/server-ts/social/facebook/index.ts new file mode 100644 index 0000000..9b21f35 --- /dev/null +++ b/modules/authentication/server-ts/social/facebook/index.ts @@ -0,0 +1,51 @@ +import { Express } from 'express'; +import passport from 'passport'; +import { Strategy as FacebookStrategy } from 'passport-facebook'; + +import { RestMethod } from '@restapp/module-server-ts'; + +import { auth, onAuthenticationSuccess } from './controllers'; +import AuthModule from '../AuthModule'; +import settings from '../../../../../settings'; + +const { + auth: { + session, + social: { + facebook: { clientID, clientSecret, callbackURL, profileFields, enabled } + } + } +} = settings; + +const beforeware = (app: Express) => { + app.use(passport.initialize()); +}; + +const onAppCreate = ({ appContext }: AuthModule) => { + passport.use( + new FacebookStrategy( + { clientID, clientSecret, callbackURL, profileFields }, + appContext.social ? appContext.social.facebook.verifyCallback : undefined + ) + ); +}; + +export default (enabled && !__TEST__ + ? new AuthModule({ + beforeware: [beforeware], + onAppCreate: [onAppCreate], + apiRouteParams: [ + { + method: RestMethod.GET, + route: 'auth/facebook', + controller: auth + }, + { + method: RestMethod.GET, + route: 'auth/facebook/callback', + middleware: [passport.authenticate('facebook', { session: session.enabled, failureRedirect: '/login' })], + controller: onAuthenticationSuccess + } + ] + }) + : undefined); diff --git a/modules/authentication/server-ts/social/github/controllers.ts b/modules/authentication/server-ts/social/github/controllers.ts new file mode 100644 index 0000000..dcf0bd1 --- /dev/null +++ b/modules/authentication/server-ts/social/github/controllers.ts @@ -0,0 +1,15 @@ +import passport from 'passport'; + +export const auth = (req: any, res: any) => { + passport.authenticate('github', { state: req.query.expoUrl })(req, res); +}; + +export const onAuthenticationSuccess = (req: any, res: any) => { + const { + locals: { appContext } + } = res; + const { t } = req; + appContext.social + ? appContext.social.github.onAuthenticationSuccess(req, res) + : res.status(500).send(t('auth:social')); +}; diff --git a/modules/authentication/server-ts/social/github/index.ts b/modules/authentication/server-ts/social/github/index.ts new file mode 100644 index 0000000..c0e00f6 --- /dev/null +++ b/modules/authentication/server-ts/social/github/index.ts @@ -0,0 +1,51 @@ +import { Express } from 'express'; +import passport from 'passport'; +import { Strategy as GitHubStrategy } from 'passport-github'; + +import { RestMethod } from '@restapp/module-server-ts'; + +import { auth, onAuthenticationSuccess } from './controllers'; +import settings from '../../../../../settings'; +import AuthModule from '../AuthModule'; + +const { + auth: { + session, + social: { + github: { clientID, clientSecret, scope, callbackURL, enabled } + } + } +} = settings; + +const beforeware = (app: Express) => { + app.use(passport.initialize()); +}; + +const onAppCreate = ({ appContext }: AuthModule) => { + passport.use( + new GitHubStrategy( + { clientID, clientSecret, scope, callbackURL }, + appContext.social ? appContext.social.github.verifyCallback : undefined + ) + ); +}; + +export default (enabled && !__TEST__ + ? new AuthModule({ + beforeware: [beforeware], + onAppCreate: [onAppCreate], + apiRouteParams: [ + { + method: RestMethod.GET, + route: 'auth/github', + controller: auth + }, + { + method: RestMethod.GET, + route: 'auth/github/callback', + middleware: [passport.authenticate('github', { session: session.enabled, failureRedirect: '/login' })], + controller: onAuthenticationSuccess + } + ] + }) + : undefined); diff --git a/modules/authentication/server-ts/social/google/controllers.ts b/modules/authentication/server-ts/social/google/controllers.ts new file mode 100644 index 0000000..8de72c8 --- /dev/null +++ b/modules/authentication/server-ts/social/google/controllers.ts @@ -0,0 +1,25 @@ +import passport from 'passport'; + +import settings from '../../../../../settings'; + +const { + auth: { + social: { + google: { scope } + } + } +} = settings; + +export const auth = (req: any, res: any) => { + passport.authenticate('google', { scope, state: req.query.expoUrl })(req, res); +}; + +export const onAuthenticationSuccess = (req: any, res: any) => { + const { + locals: { appContext } + } = res; + const { t } = req; + appContext.social + ? appContext.social.google.onAuthenticationSuccess(req, res) + : res.status(500).send(t('auth:social')); +}; diff --git a/modules/authentication/server-ts/social/google/index.ts b/modules/authentication/server-ts/social/google/index.ts new file mode 100644 index 0000000..1f40c7c --- /dev/null +++ b/modules/authentication/server-ts/social/google/index.ts @@ -0,0 +1,53 @@ +import { Express } from 'express'; +import passport from 'passport'; +import { OAuth2Strategy as GoogleStrategy } from 'passport-google-oauth'; + +import { RestMethod } from '@restapp/module-server-ts'; + +import { auth, onAuthenticationSuccess } from './controllers'; +import AuthModule from '../AuthModule'; +import settings from '../../../../../settings'; + +const { + auth: { + session, + social: { + google: { clientID, clientSecret, callbackURL, enabled } + } + } +} = settings; + +const beforeware = (app: Express) => { + app.use(passport.initialize()); +}; + +const onAppCreate = ({ appContext }: AuthModule) => { + if (enabled && !__TEST__) { + passport.use( + new GoogleStrategy( + { clientID, clientSecret, callbackURL }, + appContext.social ? appContext.social.google.verifyCallback : undefined + ) + ); + } +}; + +export default (enabled && !__TEST__ + ? new AuthModule({ + beforeware: [beforeware], + onAppCreate: [onAppCreate], + apiRouteParams: [ + { + method: RestMethod.GET, + route: 'auth/google', + controller: auth + }, + { + method: RestMethod.GET, + route: 'auth/google/callback', + middleware: [passport.authenticate('google', { session: session.enabled, failureRedirect: '/login' })], + controller: onAuthenticationSuccess + } + ] + }) + : undefined); diff --git a/modules/authentication/server-ts/social/index.ts b/modules/authentication/server-ts/social/index.ts new file mode 100644 index 0000000..38d571a --- /dev/null +++ b/modules/authentication/server-ts/social/index.ts @@ -0,0 +1,7 @@ +import ServerModule from '@restapp/module-server-ts'; +import github from './github'; +import google from './google'; +import facebook from './facebook'; +import linkedin from './linkedIn'; + +export default new ServerModule(github, google, facebook, linkedin); diff --git a/modules/authentication/server-ts/social/linkedIn/controllers.ts b/modules/authentication/server-ts/social/linkedIn/controllers.ts new file mode 100644 index 0000000..355bae4 --- /dev/null +++ b/modules/authentication/server-ts/social/linkedIn/controllers.ts @@ -0,0 +1,15 @@ +import passport from 'passport'; + +export const auth = (req: any, res: any) => { + passport.authenticate('linkedin', { state: req.query.expoUrl })(req, res); +}; + +export const onAuthenticationSuccess = (req: any, res: any) => { + const { + locals: { appContext } + } = res; + const { t } = req; + appContext.social + ? appContext.social.google.onAuthenticationSuccess(req, res) + : res.status(500).send(t('auth:social')); +}; diff --git a/modules/authentication/server-ts/social/linkedIn/index.ts b/modules/authentication/server-ts/social/linkedIn/index.ts new file mode 100644 index 0000000..a4ce503 --- /dev/null +++ b/modules/authentication/server-ts/social/linkedIn/index.ts @@ -0,0 +1,51 @@ +import { Express } from 'express'; +import passport from 'passport'; +import { Strategy as LinkedInStrategy } from '@sokratis/passport-linkedin-oauth2'; + +import { RestMethod } from '@restapp/module-server-ts'; + +import { auth, onAuthenticationSuccess } from './controllers'; +import AuthModule from '../AuthModule'; +import settings from '../../../../../settings'; + +const { + auth: { + session, + social: { + linkedin: { clientID, clientSecret, callbackURL, enabled, scope } + } + } +} = settings; + +const beforeware = (app: Express) => { + app.use(passport.initialize()); +}; + +const onAppCreate = ({ appContext }: AuthModule) => { + passport.use( + new LinkedInStrategy( + { clientID, clientSecret, callbackURL, scopeSeparator: scope }, + appContext.social ? appContext.social.linkedin.verifyCallback : undefined + ) + ); +}; + +export default (enabled && !__TEST__ + ? new AuthModule({ + beforeware: [beforeware], + onAppCreate: [onAppCreate], + apiRouteParams: [ + { + method: RestMethod.GET, + route: 'auth/linkedin', + controller: auth + }, + { + method: RestMethod.GET, + route: 'auth/linkedin/callback', + middleware: [passport.authenticate('linkedin', { session: session.enabled, failureRedirect: '/login' })], + controller: onAuthenticationSuccess + } + ] + }) + : undefined); From 5ed25afb2cb26c0b6b0ac723e7b454bfa9f67b7f Mon Sep 17 00:00:00 2001 From: Alexandr Belokur Date: Tue, 7 May 2019 15:43:05 +0300 Subject: [PATCH 021/104] Add types for passport --- packages/server/package.json | 1 + yarn.lock | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/packages/server/package.json b/packages/server/package.json index dcd8c09..14737c8 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -116,6 +116,7 @@ "@types/knex": "^0.14.20", "@types/mkdirp": "^0.5.2", "@types/nodemailer": "^4.6.2", + "@types/passport": "^1.0.0", "@types/react-dom": "^16.8.0", "@types/serialize-javascript": "^1.3.2", "@types/shortid": "0.0.29", diff --git a/yarn.lock b/yarn.lock index 695d965..da3bdc9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2203,6 +2203,13 @@ dependencies: "@types/node" "*" +"@types/passport@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@types/passport/-/passport-1.0.0.tgz#747fa127a747a145ff279f3df3e07c425e5ff297" + integrity sha512-R2FXqM+AgsMIym0PuKj08Ybx+GR6d2rU3b1/8OcHolJ+4ga2pRPX105wboV6hq1AJvMo2frQzYKdqXS5+4cyMw== + dependencies: + "@types/express" "*" + "@types/pdfmake@^0.1.3": version "0.1.3" resolved "https://registry.yarnpkg.com/@types/pdfmake/-/pdfmake-0.1.3.tgz#0f16bd71fc2ff1b9bed3b4ad042fd7119f08ae7e" From 72b4ba94e705a650667412b8a5d98a1e2de89f35 Mon Sep 17 00:00:00 2001 From: Alexandr Belokur Date: Tue, 7 May 2019 15:46:36 +0300 Subject: [PATCH 022/104] Add config for session and jwt --- config/auth.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/config/auth.js b/config/auth.js index 68329a8..edfb19e 100644 --- a/config/auth.js +++ b/config/auth.js @@ -1,4 +1,13 @@ export default { + secret: process.env.NODE_ENV === 'test' ? 'secret for tests' : process.env.AUTH_SECRET, + session: { + enabled: true + }, + jwt: { + enabled: false, + tokenExpiresIn: '1m', + refreshTokenExpiresIn: '7d' + }, password: { requireEmailConfirmation: false, sendPasswordChangesEmail: false, From dfda04a7a3c4dc824926c7f642f71f991f65c326 Mon Sep 17 00:00:00 2001 From: Alexandr Belokur Date: Tue, 7 May 2019 15:47:11 +0300 Subject: [PATCH 023/104] Add login conrtoller --- .../user/server-ts/password/controllers.ts | 24 ++++++++++++++++++- modules/user/server-ts/password/index.ts | 7 +++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/modules/user/server-ts/password/controllers.ts b/modules/user/server-ts/password/controllers.ts index 59c5106..156d718 100644 --- a/modules/user/server-ts/password/controllers.ts +++ b/modules/user/server-ts/password/controllers.ts @@ -1,6 +1,8 @@ import { UserShape } from './../sql'; import { pick, isEmpty } from 'lodash'; import jwt from 'jsonwebtoken'; +import passport from 'passport'; +import { access } from '@restapp/authentication-server-ts'; import { log } from '@restapp/core-common'; import { mailer } from '@restapp/mailer-server-ts'; @@ -11,10 +13,30 @@ import emailTemplate from '../emailTemplate'; import { createPasswordHash } from '.'; const { - auth: { password, secret }, + auth: { session, jwt: jwtSetting, password, secret }, app } = settings; +export const login = (req: any, res: any) => { + passport.authenticate('local', { session: session.enabled }, (err, user, info) => { + if (err || !user) { + return res.status(400).json({ + message: info ? info.message : 'Login failed', + user + }); + } + + req.login(user, { session: session.enabled }, async (loginErr: any) => { + if (loginErr) { + res.send(loginErr); + } + const tokens = jwtSetting.enabled ? await access.grantAccess(user, req, user.passwordHash) : null; + + return res.json({ user, tokens }); + }); + })(req, res); +}; + export const register = async ({ body, t }: any, res: any) => { const errors: ValidationErrors = {}; const userExists = await userDAO.getUserByUsername(body.username); diff --git a/modules/user/server-ts/password/index.ts b/modules/user/server-ts/password/index.ts index cb9ac42..0a5db44 100644 --- a/modules/user/server-ts/password/index.ts +++ b/modules/user/server-ts/password/index.ts @@ -2,7 +2,7 @@ import bcrypt from 'bcryptjs'; import ServerModule, { RestMethod } from '@restapp/module-server-ts'; -import { register } from './controllers'; +import { register, login } from './controllers'; import settings from '../../../../settings'; export const createPasswordHash = (pswd: string) => bcrypt.hash(pswd, 12); @@ -14,6 +14,11 @@ export default (settings.auth.password.enabled method: RestMethod.POST, route: 'register', controller: register + }, + { + method: RestMethod.POST, + route: 'login', + controller: login } ] }) From 08e2d22bfa0041273e986f08c6939c487dcd8df7 Mon Sep 17 00:00:00 2001 From: Ivan Isakov Date: Tue, 7 May 2019 16:32:50 +0300 Subject: [PATCH 024/104] Fix validation errors --- modules/core/common/createReduxStore.ts | 6 +++--- modules/forms/client-react/FormError.ts | 6 ++---- modules/user/client-react/actions/register.ts | 2 +- .../user/client-react/components/RegisterForm.native.tsx | 4 ++-- modules/user/client-react/components/RegisterForm.tsx | 6 +++--- modules/user/client-react/containers/Register.tsx | 4 ++-- modules/user/client-react/index.tsx | 2 +- 7 files changed, 14 insertions(+), 16 deletions(-) diff --git a/modules/core/common/createReduxStore.ts b/modules/core/common/createReduxStore.ts index edd869c..5bcda53 100644 --- a/modules/core/common/createReduxStore.ts +++ b/modules/core/common/createReduxStore.ts @@ -28,13 +28,13 @@ const requestMiddleware: Middleware = _state => next => action => { ...rest }); return data; - } catch (error) { + } catch ({ response: { data } }) { next({ type: FAIL, - payload: error, + payload: data, ...rest }); - return error; + return data; } }; diff --git a/modules/forms/client-react/FormError.ts b/modules/forms/client-react/FormError.ts index 91c7188..c4f1f57 100644 --- a/modules/forms/client-react/FormError.ts +++ b/modules/forms/client-react/FormError.ts @@ -1,13 +1,11 @@ -export const isFormError = (err: any) => err instanceof FormError; - export class FormError { private readonly _errors: { [key: string]: any }; - constructor(errorMsg: string, err?: any) { + constructor(message: string, err?: any) { if (err) { throw err; } else { - this._errors = { errorMsg }; + this._errors = { message }; } } diff --git a/modules/user/client-react/actions/register.ts b/modules/user/client-react/actions/register.ts index 9a3ee6d..365793f 100644 --- a/modules/user/client-react/actions/register.ts +++ b/modules/user/client-react/actions/register.ts @@ -5,6 +5,6 @@ import { ActionType } from '../reducers'; export default function REGISTER(value: RegisterSubmitProps) { return { types: [null, ActionType.REGISTER, null], - callAPI: async () => axios.post(`${__API_URL__}/register`, { ...value }) + callAPI: () => axios.post(`${__API_URL__}/register`, { ...value }) }; } diff --git a/modules/user/client-react/components/RegisterForm.native.tsx b/modules/user/client-react/components/RegisterForm.native.tsx index 9623215..14404d3 100644 --- a/modules/user/client-react/components/RegisterForm.native.tsx +++ b/modules/user/client-react/components/RegisterForm.native.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { View, StyleSheet } from 'react-native'; import { withFormik } from 'formik'; import KeyboardSpacer from 'react-native-keyboard-spacer'; -import { isFormError, FieldAdapter as Field } from '@restapp/forms-client-react'; +import { FieldAdapter as Field } from '@restapp/forms-client-react'; import { translate } from '@restapp/i18n-client-react'; import { RenderField, Button, primary, FormView } from '@restapp/look-client-react-native'; @@ -75,7 +75,7 @@ const RegisterFormWithFormik = withFormik, Regist validate: values => validate(values, registerFormSchema), async handleSubmit(values, { setErrors, props: { onSubmit } }) { onSubmit(values).catch((e: any) => { - if (isFormError(e)) { + if (e) { setErrors(e.errors); } else { throw e; diff --git a/modules/user/client-react/components/RegisterForm.tsx b/modules/user/client-react/components/RegisterForm.tsx index e4067b5..422dd6a 100644 --- a/modules/user/client-react/components/RegisterForm.tsx +++ b/modules/user/client-react/components/RegisterForm.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { withFormik } from 'formik'; -import { isFormError, FieldAdapter as Field } from '@restapp/forms-client-react'; +import { FieldAdapter as Field } from '@restapp/forms-client-react'; import { translate } from '@restapp/i18n-client-react'; import { match, email, minLength, required, validate } from '@restapp/validation-common-react'; @@ -42,7 +42,7 @@ const RegisterForm = ({ values, handleSubmit, submitting, errors, t }: FormProps value={values.passwordConfirmation} />
- {errors && errors.errorMsg && {errors.errorMsg}} + {errors && errors.message && {errors.message}} @@ -56,7 +56,7 @@ const RegisterFormWithFormik = withFormik, Regist validate: values => validate(values, registerFormSchema), async handleSubmit(values, { setErrors, props: { onSubmit } }) { onSubmit(values).catch((e: any) => { - if (isFormError(e)) { + if (e) { setErrors(e.errors); } else { throw e; diff --git a/modules/user/client-react/containers/Register.tsx b/modules/user/client-react/containers/Register.tsx index 8154d75..5fbfb85 100644 --- a/modules/user/client-react/containers/Register.tsx +++ b/modules/user/client-react/containers/Register.tsx @@ -22,8 +22,8 @@ const Register: React.FunctionComponent = props => { const onSubmit = async (values: RegisterSubmitProps) => { const data = await register(values); - if (data.error) { - throw new FormError(t('reg.errorMsg'), data.error.errors); + if (data.errors) { + throw new FormError(t('reg.errorMsg'), data); } if (!settings.auth.password.requireEmailConfirmation) { diff --git a/modules/user/client-react/index.tsx b/modules/user/client-react/index.tsx index 8014aad..0f2f7ac 100644 --- a/modules/user/client-react/index.tsx +++ b/modules/user/client-react/index.tsx @@ -66,7 +66,7 @@ interface HandleSubmitProps

{ } interface Errors { - errorMsg?: string; + message?: string; } export interface FormProps { handleSubmit: (values: V, props: HandleSubmitProps) => void; From a7e0c7d9c4820ec1db0b2b2dbb39aedfab800854 Mon Sep 17 00:00:00 2001 From: Alexandr Belokur Date: Tue, 7 May 2019 16:25:49 +0300 Subject: [PATCH 025/104] Remove social auth --- modules/authentication/server-ts/index.ts | 6 +-- .../server-ts/social/AuthModule.ts | 13 ----- .../server-ts/social/facebook/controllers.ts | 15 ------ .../server-ts/social/facebook/index.ts | 51 ------------------ .../server-ts/social/github/controllers.ts | 15 ------ .../server-ts/social/github/index.ts | 51 ------------------ .../server-ts/social/google/controllers.ts | 25 --------- .../server-ts/social/google/index.ts | 53 ------------------- .../authentication/server-ts/social/index.ts | 7 --- .../server-ts/social/linkedIn/controllers.ts | 15 ------ .../server-ts/social/linkedIn/index.ts | 51 ------------------ 11 files changed, 2 insertions(+), 300 deletions(-) delete mode 100644 modules/authentication/server-ts/social/AuthModule.ts delete mode 100644 modules/authentication/server-ts/social/facebook/controllers.ts delete mode 100644 modules/authentication/server-ts/social/facebook/index.ts delete mode 100644 modules/authentication/server-ts/social/github/controllers.ts delete mode 100644 modules/authentication/server-ts/social/github/index.ts delete mode 100644 modules/authentication/server-ts/social/google/controllers.ts delete mode 100644 modules/authentication/server-ts/social/google/index.ts delete mode 100644 modules/authentication/server-ts/social/index.ts delete mode 100644 modules/authentication/server-ts/social/linkedIn/controllers.ts delete mode 100644 modules/authentication/server-ts/social/linkedIn/index.ts diff --git a/modules/authentication/server-ts/index.ts b/modules/authentication/server-ts/index.ts index 29e12d5..62f2a4d 100644 --- a/modules/authentication/server-ts/index.ts +++ b/modules/authentication/server-ts/index.ts @@ -1,7 +1,5 @@ import ServerModule from '@restapp/module-server-ts'; -import AuthModule from './social/AuthModule'; import access from './access'; -import social from './social'; -export default new ServerModule(access, social); -export { access, AuthModule }; +export default new ServerModule(access); +export { access }; diff --git a/modules/authentication/server-ts/social/AuthModule.ts b/modules/authentication/server-ts/social/AuthModule.ts deleted file mode 100644 index 4cf31cc..0000000 --- a/modules/authentication/server-ts/social/AuthModule.ts +++ /dev/null @@ -1,13 +0,0 @@ -import ServerModule, { ServerModuleShape } from '@restapp/module-server-ts'; - -interface AuthModuleShape extends ServerModuleShape {} - -interface AuthModule extends AuthModuleShape {} - -class AuthModule extends ServerModule { - constructor(...modules: AuthModuleShape[]) { - super(...modules); - } -} - -export default AuthModule; diff --git a/modules/authentication/server-ts/social/facebook/controllers.ts b/modules/authentication/server-ts/social/facebook/controllers.ts deleted file mode 100644 index 37ee72c..0000000 --- a/modules/authentication/server-ts/social/facebook/controllers.ts +++ /dev/null @@ -1,15 +0,0 @@ -import passport from 'passport'; - -export const auth = (req: any, res: any) => { - passport.authenticate('facebook', { state: req.query.expoUrl })(req, res); -}; - -export const onAuthenticationSuccess = (req: any, res: any) => { - const { - locals: { appContext } - } = res; - const { t } = req; - appContext.social - ? appContext.social.facebook.onAuthenticationSuccess(req, res) - : res.status(500).send(t('auth:social')); -}; diff --git a/modules/authentication/server-ts/social/facebook/index.ts b/modules/authentication/server-ts/social/facebook/index.ts deleted file mode 100644 index 9b21f35..0000000 --- a/modules/authentication/server-ts/social/facebook/index.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Express } from 'express'; -import passport from 'passport'; -import { Strategy as FacebookStrategy } from 'passport-facebook'; - -import { RestMethod } from '@restapp/module-server-ts'; - -import { auth, onAuthenticationSuccess } from './controllers'; -import AuthModule from '../AuthModule'; -import settings from '../../../../../settings'; - -const { - auth: { - session, - social: { - facebook: { clientID, clientSecret, callbackURL, profileFields, enabled } - } - } -} = settings; - -const beforeware = (app: Express) => { - app.use(passport.initialize()); -}; - -const onAppCreate = ({ appContext }: AuthModule) => { - passport.use( - new FacebookStrategy( - { clientID, clientSecret, callbackURL, profileFields }, - appContext.social ? appContext.social.facebook.verifyCallback : undefined - ) - ); -}; - -export default (enabled && !__TEST__ - ? new AuthModule({ - beforeware: [beforeware], - onAppCreate: [onAppCreate], - apiRouteParams: [ - { - method: RestMethod.GET, - route: 'auth/facebook', - controller: auth - }, - { - method: RestMethod.GET, - route: 'auth/facebook/callback', - middleware: [passport.authenticate('facebook', { session: session.enabled, failureRedirect: '/login' })], - controller: onAuthenticationSuccess - } - ] - }) - : undefined); diff --git a/modules/authentication/server-ts/social/github/controllers.ts b/modules/authentication/server-ts/social/github/controllers.ts deleted file mode 100644 index dcf0bd1..0000000 --- a/modules/authentication/server-ts/social/github/controllers.ts +++ /dev/null @@ -1,15 +0,0 @@ -import passport from 'passport'; - -export const auth = (req: any, res: any) => { - passport.authenticate('github', { state: req.query.expoUrl })(req, res); -}; - -export const onAuthenticationSuccess = (req: any, res: any) => { - const { - locals: { appContext } - } = res; - const { t } = req; - appContext.social - ? appContext.social.github.onAuthenticationSuccess(req, res) - : res.status(500).send(t('auth:social')); -}; diff --git a/modules/authentication/server-ts/social/github/index.ts b/modules/authentication/server-ts/social/github/index.ts deleted file mode 100644 index c0e00f6..0000000 --- a/modules/authentication/server-ts/social/github/index.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Express } from 'express'; -import passport from 'passport'; -import { Strategy as GitHubStrategy } from 'passport-github'; - -import { RestMethod } from '@restapp/module-server-ts'; - -import { auth, onAuthenticationSuccess } from './controllers'; -import settings from '../../../../../settings'; -import AuthModule from '../AuthModule'; - -const { - auth: { - session, - social: { - github: { clientID, clientSecret, scope, callbackURL, enabled } - } - } -} = settings; - -const beforeware = (app: Express) => { - app.use(passport.initialize()); -}; - -const onAppCreate = ({ appContext }: AuthModule) => { - passport.use( - new GitHubStrategy( - { clientID, clientSecret, scope, callbackURL }, - appContext.social ? appContext.social.github.verifyCallback : undefined - ) - ); -}; - -export default (enabled && !__TEST__ - ? new AuthModule({ - beforeware: [beforeware], - onAppCreate: [onAppCreate], - apiRouteParams: [ - { - method: RestMethod.GET, - route: 'auth/github', - controller: auth - }, - { - method: RestMethod.GET, - route: 'auth/github/callback', - middleware: [passport.authenticate('github', { session: session.enabled, failureRedirect: '/login' })], - controller: onAuthenticationSuccess - } - ] - }) - : undefined); diff --git a/modules/authentication/server-ts/social/google/controllers.ts b/modules/authentication/server-ts/social/google/controllers.ts deleted file mode 100644 index 8de72c8..0000000 --- a/modules/authentication/server-ts/social/google/controllers.ts +++ /dev/null @@ -1,25 +0,0 @@ -import passport from 'passport'; - -import settings from '../../../../../settings'; - -const { - auth: { - social: { - google: { scope } - } - } -} = settings; - -export const auth = (req: any, res: any) => { - passport.authenticate('google', { scope, state: req.query.expoUrl })(req, res); -}; - -export const onAuthenticationSuccess = (req: any, res: any) => { - const { - locals: { appContext } - } = res; - const { t } = req; - appContext.social - ? appContext.social.google.onAuthenticationSuccess(req, res) - : res.status(500).send(t('auth:social')); -}; diff --git a/modules/authentication/server-ts/social/google/index.ts b/modules/authentication/server-ts/social/google/index.ts deleted file mode 100644 index 1f40c7c..0000000 --- a/modules/authentication/server-ts/social/google/index.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { Express } from 'express'; -import passport from 'passport'; -import { OAuth2Strategy as GoogleStrategy } from 'passport-google-oauth'; - -import { RestMethod } from '@restapp/module-server-ts'; - -import { auth, onAuthenticationSuccess } from './controllers'; -import AuthModule from '../AuthModule'; -import settings from '../../../../../settings'; - -const { - auth: { - session, - social: { - google: { clientID, clientSecret, callbackURL, enabled } - } - } -} = settings; - -const beforeware = (app: Express) => { - app.use(passport.initialize()); -}; - -const onAppCreate = ({ appContext }: AuthModule) => { - if (enabled && !__TEST__) { - passport.use( - new GoogleStrategy( - { clientID, clientSecret, callbackURL }, - appContext.social ? appContext.social.google.verifyCallback : undefined - ) - ); - } -}; - -export default (enabled && !__TEST__ - ? new AuthModule({ - beforeware: [beforeware], - onAppCreate: [onAppCreate], - apiRouteParams: [ - { - method: RestMethod.GET, - route: 'auth/google', - controller: auth - }, - { - method: RestMethod.GET, - route: 'auth/google/callback', - middleware: [passport.authenticate('google', { session: session.enabled, failureRedirect: '/login' })], - controller: onAuthenticationSuccess - } - ] - }) - : undefined); diff --git a/modules/authentication/server-ts/social/index.ts b/modules/authentication/server-ts/social/index.ts deleted file mode 100644 index 38d571a..0000000 --- a/modules/authentication/server-ts/social/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import ServerModule from '@restapp/module-server-ts'; -import github from './github'; -import google from './google'; -import facebook from './facebook'; -import linkedin from './linkedIn'; - -export default new ServerModule(github, google, facebook, linkedin); diff --git a/modules/authentication/server-ts/social/linkedIn/controllers.ts b/modules/authentication/server-ts/social/linkedIn/controllers.ts deleted file mode 100644 index 355bae4..0000000 --- a/modules/authentication/server-ts/social/linkedIn/controllers.ts +++ /dev/null @@ -1,15 +0,0 @@ -import passport from 'passport'; - -export const auth = (req: any, res: any) => { - passport.authenticate('linkedin', { state: req.query.expoUrl })(req, res); -}; - -export const onAuthenticationSuccess = (req: any, res: any) => { - const { - locals: { appContext } - } = res; - const { t } = req; - appContext.social - ? appContext.social.google.onAuthenticationSuccess(req, res) - : res.status(500).send(t('auth:social')); -}; diff --git a/modules/authentication/server-ts/social/linkedIn/index.ts b/modules/authentication/server-ts/social/linkedIn/index.ts deleted file mode 100644 index a4ce503..0000000 --- a/modules/authentication/server-ts/social/linkedIn/index.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Express } from 'express'; -import passport from 'passport'; -import { Strategy as LinkedInStrategy } from '@sokratis/passport-linkedin-oauth2'; - -import { RestMethod } from '@restapp/module-server-ts'; - -import { auth, onAuthenticationSuccess } from './controllers'; -import AuthModule from '../AuthModule'; -import settings from '../../../../../settings'; - -const { - auth: { - session, - social: { - linkedin: { clientID, clientSecret, callbackURL, enabled, scope } - } - } -} = settings; - -const beforeware = (app: Express) => { - app.use(passport.initialize()); -}; - -const onAppCreate = ({ appContext }: AuthModule) => { - passport.use( - new LinkedInStrategy( - { clientID, clientSecret, callbackURL, scopeSeparator: scope }, - appContext.social ? appContext.social.linkedin.verifyCallback : undefined - ) - ); -}; - -export default (enabled && !__TEST__ - ? new AuthModule({ - beforeware: [beforeware], - onAppCreate: [onAppCreate], - apiRouteParams: [ - { - method: RestMethod.GET, - route: 'auth/linkedin', - controller: auth - }, - { - method: RestMethod.GET, - route: 'auth/linkedin/callback', - middleware: [passport.authenticate('linkedin', { session: session.enabled, failureRedirect: '/login' })], - controller: onAuthenticationSuccess - } - ] - }) - : undefined); From acc851e942bcc944b5d68602eec2e75e1b71d9c9 Mon Sep 17 00:00:00 2001 From: Alexandr Belokur Date: Tue, 7 May 2019 16:33:36 +0300 Subject: [PATCH 026/104] Update types for auth module --- packages/server/package.json | 11 ++ packages/server/typings/typings.d.ts | 1 + yarn.lock | 199 +++++++++++++++++++++++++-- 3 files changed, 199 insertions(+), 12 deletions(-) diff --git a/packages/server/package.json b/packages/server/package.json index 14737c8..9f62278 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -51,17 +51,20 @@ "@babel/polyfill": "^7.0.0", "@babel/preset-env": "^7.0.0", "@babel/register": "^7.0.0", + "@restapp/authentication-server-ts": "^0.1.0", "@restapp/core-common": "^0.1.0", "@restapp/database-server-ts": "^0.1.0", "@restapp/testing-server-ts": "^0.1.0", "@restapp/user-server-ts": "^0.1.0", "@restapp/welcome-server-ts": "^0.1.0", + "@sokratis/passport-linkedin-oauth2": "^2.0.2", "@types/pdfmake": "^0.1.3", "bcryptjs": "^2.4.3", "body-parser": "^1.18.3", "dotenv": "^5.0.1", "error-stack-parser": "^2.0.1", "express": "^4.16.2", + "express-session": "^1.16.1", "filesize": "^3.5.11", "humps": "^2.0.1", "i18next": "^11.3.3", @@ -79,6 +82,7 @@ "passport-facebook": "^2.1.1", "passport-github": "^1.1.0", "passport-google-oauth": "^1.0.0", + "passport-jwt": "^4.0.0", "passport-linkedin-oauth2": "^1.5.0", "passport-local": "^1.0.0", "performance-now": "^2.1.0", @@ -95,6 +99,7 @@ "redux": "^4.0.0", "roboto-npm-webfont": "^1.0.1", "serialize-javascript": "^1.4.0", + "session-file-store": "^1.2.0", "shortid": "^2.2.8", "source-map-support": "^0.5.0", "sourcemapped-stacktrace": "^1.1.8", @@ -109,6 +114,7 @@ "@types/bcryptjs": "^2.4.2", "@types/chai": "^4.1.4", "@types/chai-http": "^3.0.5", + "@types/express-session": "^1.15.12", "@types/humps": "^1.1.2", "@types/i18next": "^8.4.4", "@types/i18next-express-middleware": "^0.0.33", @@ -117,6 +123,11 @@ "@types/mkdirp": "^0.5.2", "@types/nodemailer": "^4.6.2", "@types/passport": "^1.0.0", + "@types/passport-facebook": "^2.1.9", + "@types/passport-github": "^1.1.5", + "@types/passport-google-oauth": "^1.0.41", + "@types/passport-jwt": "^3.0.1", + "@types/passport-local": "^1.0.33", "@types/react-dom": "^16.8.0", "@types/serialize-javascript": "^1.3.2", "@types/shortid": "0.0.29", diff --git a/packages/server/typings/typings.d.ts b/packages/server/typings/typings.d.ts index 13c5dc3..9a48eb4 100644 --- a/packages/server/typings/typings.d.ts +++ b/packages/server/typings/typings.d.ts @@ -15,3 +15,4 @@ declare var global: Global; // packages without types declare module 'universal-cookie-express'; +declare module '@sokratis/passport-linkedin-oauth2'; \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index da3bdc9..9025673 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1980,6 +1980,14 @@ resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd" integrity sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow== +"@sokratis/passport-linkedin-oauth2@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@sokratis/passport-linkedin-oauth2/-/passport-linkedin-oauth2-2.0.2.tgz#30d7dbc5ea44e67d8a90a3e311a908185064c4dc" + integrity sha512-b83BZHrNT9U/PBYiuAaCY3XUC0QYauFS4s88LWZKDTZ2G+6mYZ3cBObLTAHMK7Qt84nWwmYV1Ul2DSCLlqCxZQ== + dependencies: + passport-oauth2 "1.x.x" + underscore "^1.7.0" + "@types/bcryptjs@^2.4.2": version "2.4.2" resolved "https://registry.yarnpkg.com/@types/bcryptjs/-/bcryptjs-2.4.2.tgz#e3530eac9dd136bfdfb0e43df2c4c5ce1f77dfae" @@ -2062,6 +2070,14 @@ "@types/node" "*" "@types/range-parser" "*" +"@types/express-session@^1.15.12": + version "1.15.12" + resolved "https://registry.yarnpkg.com/@types/express-session/-/express-session-1.15.12.tgz#1126704826e80f8381da4fbbb35a199f550d1433" + integrity sha512-DHZXzWy6Nu5Ng0syXUiVFRpZ6/1DOXoTCWa6RG3itGrub2ioBYvgtDbkT6VHHNo3iOdHRROyWANsMBJVaflblQ== + dependencies: + "@types/express" "*" + "@types/node" "*" + "@types/express@*": version "4.16.1" resolved "https://registry.yarnpkg.com/@types/express/-/express-4.16.1.tgz#d756bd1a85c34d87eaf44c888bad27ba8a4b7cf0" @@ -2142,7 +2158,7 @@ "@types/tough-cookie" "*" parse5 "^4.0.0" -"@types/jsonwebtoken@^8.3.2": +"@types/jsonwebtoken@*", "@types/jsonwebtoken@^8.3.2": version "8.3.2" resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-8.3.2.tgz#e3d5245197152346fae7ee87d5541aa5a92d0362" integrity sha512-Mkjljd9DTpkPlrmGfTJvcP4aBU7yO2QmW7wNVhV4/6AEUxYoacqU7FJU/N0yFEHTsIrE4da3rUrjrR5ejicFmA== @@ -2203,7 +2219,74 @@ dependencies: "@types/node" "*" -"@types/passport@^1.0.0": +"@types/oauth@*": + version "0.9.1" + resolved "https://registry.yarnpkg.com/@types/oauth/-/oauth-0.9.1.tgz#e17221e7f7936b0459ae7d006255dff61adca305" + integrity sha512-a1iY62/a3yhZ7qH7cNUsxoI3U/0Fe9+RnuFrpTKr+0WVOzbKlSLojShCKe20aOD1Sppv+i8Zlq0pLDuTJnwS4A== + dependencies: + "@types/node" "*" + +"@types/passport-facebook@^2.1.9": + version "2.1.9" + resolved "https://registry.yarnpkg.com/@types/passport-facebook/-/passport-facebook-2.1.9.tgz#0181b40be4e002705f7a29d64da9029c2ab464dc" + integrity sha512-7Y5aHo35Io9fRvStBgXasf+6rRJ5SMiu1D3+cnUuxM1GS+QlgFnH+YTZjye0XgF4EWzxlXKTeLlK2DFU8+0Jpw== + dependencies: + "@types/express" "*" + "@types/passport" "*" + +"@types/passport-github@^1.1.5": + version "1.1.5" + resolved "https://registry.yarnpkg.com/@types/passport-github/-/passport-github-1.1.5.tgz#57ea20d1de0789b98a7157603d76d87c271de6b2" + integrity sha512-BeWDdLRWfPpJcmT1XofY5r1Av//TcxBEgllY4LnArcYdGqbIIVLyHwR+8bIG+ZC4PwJ6W1trnVEG3EQ+5J+Jmw== + dependencies: + "@types/express" "*" + "@types/passport" "*" + "@types/passport-oauth2" "*" + +"@types/passport-google-oauth@^1.0.41": + version "1.0.41" + resolved "https://registry.yarnpkg.com/@types/passport-google-oauth/-/passport-google-oauth-1.0.41.tgz#e24a8a070dcc5139f0a205c72730b14e83c1405a" + integrity sha512-sl79Oau92bilbq6Sv47StzEt98qhu3kMjOBK4GaJX6XtG2a3bwVEebYkwxPrJkz7wb9S1O9x2Dtb3jMqqj9j9Q== + dependencies: + "@types/express" "*" + "@types/passport" "*" + +"@types/passport-jwt@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/passport-jwt/-/passport-jwt-3.0.1.tgz#bc4c2610815565de977ea1a580c047d71c646084" + integrity sha512-JwF9U/Rmr6YicHSu/MITmHNDy2KeiedxKW2bhz6wZts3y4cq48NiN0UD98zO56TyM5Vm6BpyjFxcs6jh68ni/A== + dependencies: + "@types/express" "*" + "@types/jsonwebtoken" "*" + "@types/passport-strategy" "*" + +"@types/passport-local@^1.0.33": + version "1.0.33" + resolved "https://registry.yarnpkg.com/@types/passport-local/-/passport-local-1.0.33.tgz#d245b60c5b801cb3aeca1ffab557d5fe1534260d" + integrity sha512-+rn6ZIxje0jZ2+DAiWFI8vGG7ZFKB0hXx2cUdMmudSWsigSq6ES7Emso46r4HJk0qCgrZVfI8sJiM7HIYf4SbA== + dependencies: + "@types/express" "*" + "@types/passport" "*" + "@types/passport-strategy" "*" + +"@types/passport-oauth2@*": + version "1.4.8" + resolved "https://registry.yarnpkg.com/@types/passport-oauth2/-/passport-oauth2-1.4.8.tgz#b11cb3323970a88db3d170fe3d4dcdd0af02d8bf" + integrity sha512-tlX16wyFE5YJR2pHpZ308dgB1MV9/Ra2wfQh71eWk+/umPoD1Rca2D4N5M27W7nZm1wqUNGTk1I864nHvEgiFA== + dependencies: + "@types/express" "*" + "@types/oauth" "*" + "@types/passport" "*" + +"@types/passport-strategy@*": + version "0.2.35" + resolved "https://registry.yarnpkg.com/@types/passport-strategy/-/passport-strategy-0.2.35.tgz#e52f5212279ea73f02d9b06af67efe9cefce2d0c" + integrity sha512-o5D19Jy2XPFoX2rKApykY15et3Apgax00RRLf0RUotPDUsYrQa7x4howLYr9El2mlUApHmCMv5CZ1IXqKFQ2+g== + dependencies: + "@types/express" "*" + "@types/passport" "*" + +"@types/passport@*", "@types/passport@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/passport/-/passport-1.0.0.tgz#747fa127a747a145ff279f3df3e07c425e5ff297" integrity sha512-R2FXqM+AgsMIym0PuKj08Ybx+GR6d2rU3b1/8OcHolJ+4ga2pRPX105wboV6hq1AJvMo2frQzYKdqXS5+4cyMw== @@ -4102,6 +4185,11 @@ babylon@^6.18.0: resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" integrity sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ== +bagpipe@^0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/bagpipe/-/bagpipe-0.3.5.tgz#e341d164fcb24cdf04ea7e05b765ec10c8aea6a1" + integrity sha1-40HRZPyyTN8E6n4Ft2XsEMiupqE= + balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" @@ -6144,6 +6232,11 @@ depd@~1.1.1, depd@~1.1.2: resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= +depd@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + deprecation@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-1.0.1.tgz#2df79b79005752180816b7b6e079cbd80490d711" @@ -7539,6 +7632,20 @@ expo@^32.0.0: uuid-js "^0.7.5" whatwg-fetch "^2.0.4" +express-session@^1.16.1: + version "1.16.1" + resolved "https://registry.yarnpkg.com/express-session/-/express-session-1.16.1.tgz#251ff9776c59382301de6c8c33411af357ed439c" + integrity sha512-pWvUL8Tl5jUy1MLH7DhgUlpoKeVPUTe+y6WQD9YhcN0C5qAhsh4a8feVjiUXo3TFhIy191YGZ4tewW9edbl2xQ== + dependencies: + cookie "0.3.1" + cookie-signature "1.0.6" + debug "2.6.9" + depd "~2.0.0" + on-headers "~1.0.2" + parseurl "~1.3.2" + safe-buffer "5.1.2" + uid-safe "~2.1.5" + express@4.16.4, express@^4.13.4, express@^4.15.2, express@^4.16.2: version "4.16.4" resolved "https://registry.yarnpkg.com/express/-/express-4.16.4.tgz#fddef61926109e24c515ea97fd2f1bdbf62df12e" @@ -8130,7 +8237,7 @@ fs-extra@^2.0.0: graceful-fs "^4.1.2" jsonfile "^2.1.0" -fs-extra@^4.0.1, fs-extra@^4.0.2: +fs-extra@^4.0.0, fs-extra@^4.0.1, fs-extra@^4.0.2: version "4.0.3" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.3.tgz#0d852122e5bc5beb453fb028e9c0c9bf36340c94" integrity sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg== @@ -9198,7 +9305,7 @@ immediate@^3.2.2: resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.2.3.tgz#d140fa8f614659bd6541233097ddaac25cdd991c" integrity sha1-0UD6j2FGWb1lQSMwl92qwlzdmRw= -immutability-helper@2.8.1, immutability-helper@^2.6.2: +immutability-helper@^2.6.2: version "2.8.1" resolved "https://registry.yarnpkg.com/immutability-helper/-/immutability-helper-2.8.1.tgz#3c5ec05fcd83676bfae7146f319595243ad904f4" integrity sha512-8AVB5EUpRBUdXqfe4cFsFECsOIZ9hX/Arl8B8S9/tmwpYv3UWvOsXUPOjkuZIMaVxfSWkxCzkng1rjmEoSWrxQ== @@ -10047,11 +10154,6 @@ items@2.x.x: resolved "https://registry.yarnpkg.com/items/-/items-2.1.2.tgz#0849354595805d586dac98e7e6e85556ea838558" integrity sha512-kezcEqgB97BGeZZYtX/MA8AG410ptURstvnz5RAgyFZ8wQFPMxHY8GpTq+/ZHKT3frSlIthUq7EvLt9xn3TvXg== -iterall@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.2.2.tgz#92d70deb8028e0c39ff3164fdbf4d8b088130cd7" - integrity sha512-yynBb1g+RFUPY64fTrFv7nsjRrENBQJaX2UL+2Szc9REFrSNm1rpSXHGzhmAy7a9uv3vlvgBlXnf9RqmPH1/DA== - jest-changed-files@^23.4.2: version "23.4.2" resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-23.4.2.tgz#1eed688370cd5eebafe4ae93d34bb3b64968fe83" @@ -10667,6 +10769,22 @@ jsonwebtoken@^8.1.0: ms "^2.1.1" semver "^5.6.0" +jsonwebtoken@^8.2.0: + version "8.5.1" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" + integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w== + dependencies: + jws "^3.2.2" + lodash.includes "^4.3.0" + lodash.isboolean "^3.0.3" + lodash.isinteger "^4.0.4" + lodash.isnumber "^3.0.3" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.once "^4.0.0" + ms "^2.1.1" + semver "^5.6.0" + jsprim@^1.2.2: version "1.4.1" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" @@ -10693,6 +10811,15 @@ jwa@^1.2.0: ecdsa-sig-formatter "1.0.11" safe-buffer "^5.0.1" +jwa@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" + integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + jws@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.1.tgz#d79d4216a62c9afa0a3d5e8b5356d75abdeb2be5" @@ -10701,6 +10828,14 @@ jws@^3.2.1: jwa "^1.2.0" safe-buffer "^5.0.1" +jws@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" + integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== + dependencies: + jwa "^1.4.1" + safe-buffer "^5.0.1" + jwt-decode@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-2.2.0.tgz#7d86bd56679f58ce6a84704a657dd392bba81a79" @@ -13048,7 +13183,7 @@ on-finished@~2.3.0: dependencies: ee-first "1.1.1" -on-headers@~1.0.1: +on-headers@~1.0.1, on-headers@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== @@ -13585,6 +13720,14 @@ passport-google-oauth@^1.0.0: passport-google-oauth1 "1.x.x" passport-google-oauth20 "1.x.x" +passport-jwt@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/passport-jwt/-/passport-jwt-4.0.0.tgz#7f0be7ba942e28b9f5d22c2ebbb8ce96ef7cf065" + integrity sha512-BwC0n2GP/1hMVjR4QpnvqA61TxenUMlmfNjYNgK0ZAs0HK4SOQkHcSv4L328blNTLtHq7DbmvyNJiH+bn6C5Mg== + dependencies: + jsonwebtoken "^8.2.0" + passport-strategy "^1.0.0" + passport-linkedin-oauth2@^1.5.0: version "1.6.1" resolved "https://registry.yarnpkg.com/passport-linkedin-oauth2/-/passport-linkedin-oauth2-1.6.1.tgz#e540db935d9bfd65b0179f4bb424bd5d87d3f76e" @@ -13619,7 +13762,7 @@ passport-oauth2@1.x.x: uid2 "0.0.x" utils-merge "1.x.x" -passport-strategy@1.x.x: +passport-strategy@1.x.x, passport-strategy@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4" integrity sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ= @@ -14383,6 +14526,11 @@ ramda@^0.26.1: resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.26.1.tgz#8d41351eb8111c55353617fc3bbffad8e4d35d06" integrity sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ== +random-bytes@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b" + integrity sha1-T2ih3Arli9P7lYSMMDJNt11kNgs= + randomatic@^3.0.0: version "3.1.1" resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-3.1.1.tgz#b776efc59375984e36c537b2f51a1f0aff0da1ed" @@ -16531,6 +16679,17 @@ serve-static@1.13.2, serve-static@^1.13.1: parseurl "~1.3.2" send "0.16.2" +session-file-store@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/session-file-store/-/session-file-store-1.2.0.tgz#35a994a7fb0cbf7f784067bf9059939c520b7c44" + integrity sha512-DkYLYFkkK6u9xyraVHemulhlUuuufLukf7SQxOZSx8SPwkswcaIrls882PaQZ72zRKsyhUVNxOUl9w0lQubUFw== + dependencies: + bagpipe "^0.3.5" + fs-extra "^4.0.0" + object-assign "^4.1.1" + retry "^0.10.0" + write-file-atomic "1.3.1" + set-blocking@^2.0.0, set-blocking@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" @@ -17913,6 +18072,13 @@ uid-number@0.0.6: resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" integrity sha1-DqEOgDXo61uOREnwbaHHMGY7qoE= +uid-safe@~2.1.5: + version "2.1.5" + resolved "https://registry.yarnpkg.com/uid-safe/-/uid-safe-2.1.5.tgz#2b3d5c7240e8fc2e58f8aa269e5ee49c0857bd3a" + integrity sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA== + dependencies: + random-bytes "~1.0.0" + uid2@0.0.x: version "0.0.3" resolved "https://registry.yarnpkg.com/uid2/-/uid2-0.0.3.tgz#483126e11774df2f71b8b639dcd799c376162b82" @@ -18343,7 +18509,7 @@ watch@~0.18.0: exec-sh "^0.2.0" minimist "^1.2.0" -watchpack@^1.5.0, "watchpack@https://github.com/Globegitter/watchpack": +watchpack@^1.5.0: version "1.6.0" resolved "https://github.com/Globegitter/watchpack#d903e64a910ab068299f824b04a5e216a15672e9" dependencies: @@ -18646,6 +18812,15 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= +write-file-atomic@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-1.3.1.tgz#7d45ba32316328dd1ec7d90f60ebc0d845bb759a" + integrity sha1-fUW6MjFjKN0ex9kPYOvA2EW7dZo= + dependencies: + graceful-fs "^4.1.11" + imurmurhash "^0.1.4" + slide "^1.1.5" + write-file-atomic@^1.2.0: version "1.3.4" resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-1.3.4.tgz#f807a4f0b1d9e913ae7a48112e6cc3af1991b45f" From 24b08455e625551ecd4f7a29403490b81ea81819 Mon Sep 17 00:00:00 2001 From: Alexandr Belokur Date: Tue, 7 May 2019 16:35:17 +0300 Subject: [PATCH 027/104] Update LocalStrategy usernameField --- modules/authentication/server-ts/access/jwt/index.ts | 5 +---- modules/authentication/server-ts/access/session/index.ts | 5 +---- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/modules/authentication/server-ts/access/jwt/index.ts b/modules/authentication/server-ts/access/jwt/index.ts index 23f4d74..1856e91 100644 --- a/modules/authentication/server-ts/access/jwt/index.ts +++ b/modules/authentication/server-ts/access/jwt/index.ts @@ -1,6 +1,5 @@ import { Express } from 'express'; import { Strategy as LocalStratery } from 'passport-local'; -import bodyParser from 'body-parser'; import passport from 'passport'; import { Strategy as JWTStrategy, ExtractJwt } from 'passport-jwt'; @@ -15,8 +14,6 @@ const { } = settings; const beforeware = (app: Express) => { - app.use(bodyParser.urlencoded({ extended: false })); - app.use(bodyParser.json()); app.use(passport.initialize()); }; @@ -34,7 +31,7 @@ const grant = async (identity: any, req: any, passwordHash: string = '') => { const onAppCreate = ({ appContext }: AccessModule) => { passport.use( - new LocalStratery(async (username: string, password: string, done: any) => { + new LocalStratery({ usernameField: 'usernameOrEmail' }, async (username: string, password: string, done: any) => { const { identity, message } = await appContext.user.validateLogin(username, password); if (message) { diff --git a/modules/authentication/server-ts/access/session/index.ts b/modules/authentication/server-ts/access/session/index.ts index b73eac3..bbeda25 100644 --- a/modules/authentication/server-ts/access/session/index.ts +++ b/modules/authentication/server-ts/access/session/index.ts @@ -1,6 +1,5 @@ import { Express, Request, Response } from 'express'; import { Strategy } from 'passport-local'; -import bodyParser from 'body-parser'; import session from 'express-session'; import passport from 'passport'; @@ -13,8 +12,6 @@ import { logout } from './controllers'; const FileStore = require('session-file-store')(session); const beforeware = (app: Express) => { - app.use(bodyParser.urlencoded({ extended: false })); - app.use(bodyParser.json()); app.use( session({ secret: 'secret', @@ -42,7 +39,7 @@ const onAppCreate = ({ appContext }: AccessModule) => { }); passport.use( - new Strategy(async (username: string, password: string, done: any) => { + new Strategy({ usernameField: 'usernameOrEmail' }, async (username: string, password: string, done: any) => { const { identity, message } = await appContext.user.validateLogin(username, password); if (message) { From 9438b3498f2c16030abfcf24ddfd0f4defaf6389 Mon Sep 17 00:00:00 2001 From: Alexandr Belokur Date: Tue, 7 May 2019 16:40:07 +0300 Subject: [PATCH 028/104] Update errors response --- modules/user/server-ts/locales/en/translations.json | 1 + modules/user/server-ts/locales/ru/translations.json | 1 + modules/user/server-ts/password/controllers.ts | 6 +++--- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/modules/user/server-ts/locales/en/translations.json b/modules/user/server-ts/locales/en/translations.json index b67acdb..a230977 100644 --- a/modules/user/server-ts/locales/en/translations.json +++ b/modules/user/server-ts/locales/en/translations.json @@ -10,6 +10,7 @@ "password": { "validPasswordEmail": "Please enter a valid username or e-mail.", "emailConfirmation": "Please confirm your e-mail first.", + "registrationFailed": "Registration failed due to validation errors", "validPassword": "Please enter a valid password.", "usernameIsExisted": "Username already exists", "emailIsExisted": "E-mail already exists.", diff --git a/modules/user/server-ts/locales/ru/translations.json b/modules/user/server-ts/locales/ru/translations.json index 457fff7..b50aa94 100644 --- a/modules/user/server-ts/locales/ru/translations.json +++ b/modules/user/server-ts/locales/ru/translations.json @@ -10,6 +10,7 @@ "password": { "validPasswordEmail": "Пожалуйста, введите Ваш username или e-mail.", "emailConfirmation": "Пожалуйста, подтвердите Ваш e-mail", + "registrationFailed": "Регистрация не была успешной из-за ошибок валидации.", "validPassword": "Пожалуйста, введите действительный пароль.", "usernameIsExisted": "Данное имя пользователя уже используется", "emailIsExisted": "Данный e-mail уже используется.", diff --git a/modules/user/server-ts/password/controllers.ts b/modules/user/server-ts/password/controllers.ts index 59c5106..740d68e 100644 --- a/modules/user/server-ts/password/controllers.ts +++ b/modules/user/server-ts/password/controllers.ts @@ -29,9 +29,9 @@ export const register = async ({ body, t }: any, res: any) => { if (!isEmpty(errors)) { return res.status(422).send({ - error: { - message: 'Failed reset password', - errors + errors: { + message: t('user:auth.password.registrationFailed'), + ...errors } }); } From 71645c19fb774968901a6ca057725e241f1c7329 Mon Sep 17 00:00:00 2001 From: Ivan Isakov Date: Tue, 7 May 2019 17:34:27 +0300 Subject: [PATCH 029/104] Add uthentication module --- config/auth.js | 37 +++++++- .../client-react/access/AccessModule.ts | 49 +++++++++++ .../client-react/access/index.tsx | 48 ++++++++++ .../client-react/access/jwt/index.ts | 86 ++++++++++++++++++ .../client-react/access/session/index.ts | 13 +++ .../client-react/helpers/index.ts | 12 +++ modules/authentication/client-react/index.ts | 5 ++ .../authentication/client-react/package.json | 8 ++ .../facebook/containers/FacebookButton.css | 39 +++++++++ .../containers/FacebookButton.native.tsx | 80 +++++++++++++++++ .../facebook/containers/FacebookButton.tsx | 55 ++++++++++++ .../social/facebook/index.native.ts | 3 + .../client-react/social/facebook/index.ts | 3 + .../social/github/containers/GitHubButton.css | 39 +++++++++ .../github/containers/GitHubButton.native.tsx | 84 ++++++++++++++++++ .../social/github/containers/GitHubButton.tsx | 55 ++++++++++++ .../social/github/index.native.ts | 3 + .../client-react/social/github/index.ts | 3 + .../social/google/containers/GoogleButton.css | 39 +++++++++ .../google/containers/GoogleButton.native.tsx | 87 +++++++++++++++++++ .../social/google/containers/GoogleButton.tsx | 55 ++++++++++++ .../social/google/index.native.ts | 3 + .../client-react/social/google/index.ts | 3 + .../client-react/social/index.ts | 12 +++ .../linkedin/containers/LinkedInButton.css | 39 +++++++++ .../containers/LinkedInButton.native.tsx | 84 ++++++++++++++++++ .../linkedin/containers/LinkedInButton.tsx | 55 ++++++++++++ .../social/linkedin/index.native.ts | 3 + .../client-react/social/linkedin/index.ts | 3 + .../look/client-react-native/styles/button.js | 5 -- .../look/client-react-native/styles/button.ts | 6 ++ .../styles/{colors.js => colors.ts} | 0 .../styles/{index.js => index.ts} | 0 .../{itemContainer.js => itemContainer.ts} | 8 +- .../{socialButton.js => socialButton.ts} | 18 ++-- packages/client/package.json | 8 +- packages/client/src/modules.ts | 13 ++- packages/mobile/src/modules.ts | 3 +- yarn.lock | 40 +++++++-- 39 files changed, 1076 insertions(+), 30 deletions(-) create mode 100644 modules/authentication/client-react/access/AccessModule.ts create mode 100644 modules/authentication/client-react/access/index.tsx create mode 100644 modules/authentication/client-react/access/jwt/index.ts create mode 100644 modules/authentication/client-react/access/session/index.ts create mode 100644 modules/authentication/client-react/helpers/index.ts create mode 100644 modules/authentication/client-react/index.ts create mode 100644 modules/authentication/client-react/package.json create mode 100644 modules/authentication/client-react/social/facebook/containers/FacebookButton.css create mode 100644 modules/authentication/client-react/social/facebook/containers/FacebookButton.native.tsx create mode 100644 modules/authentication/client-react/social/facebook/containers/FacebookButton.tsx create mode 100644 modules/authentication/client-react/social/facebook/index.native.ts create mode 100644 modules/authentication/client-react/social/facebook/index.ts create mode 100644 modules/authentication/client-react/social/github/containers/GitHubButton.css create mode 100644 modules/authentication/client-react/social/github/containers/GitHubButton.native.tsx create mode 100644 modules/authentication/client-react/social/github/containers/GitHubButton.tsx create mode 100644 modules/authentication/client-react/social/github/index.native.ts create mode 100644 modules/authentication/client-react/social/github/index.ts create mode 100644 modules/authentication/client-react/social/google/containers/GoogleButton.css create mode 100644 modules/authentication/client-react/social/google/containers/GoogleButton.native.tsx create mode 100644 modules/authentication/client-react/social/google/containers/GoogleButton.tsx create mode 100644 modules/authentication/client-react/social/google/index.native.ts create mode 100644 modules/authentication/client-react/social/google/index.ts create mode 100644 modules/authentication/client-react/social/index.ts create mode 100644 modules/authentication/client-react/social/linkedin/containers/LinkedInButton.css create mode 100644 modules/authentication/client-react/social/linkedin/containers/LinkedInButton.native.tsx create mode 100644 modules/authentication/client-react/social/linkedin/containers/LinkedInButton.tsx create mode 100644 modules/authentication/client-react/social/linkedin/index.native.ts create mode 100644 modules/authentication/client-react/social/linkedin/index.ts delete mode 100644 modules/look/client-react-native/styles/button.js create mode 100644 modules/look/client-react-native/styles/button.ts rename modules/look/client-react-native/styles/{colors.js => colors.ts} (100%) rename modules/look/client-react-native/styles/{index.js => index.ts} (100%) rename modules/look/client-react-native/styles/{itemContainer.js => itemContainer.ts} (52%) rename modules/look/client-react-native/styles/{socialButton.js => socialButton.ts} (68%) diff --git a/config/auth.js b/config/auth.js index edfb19e..61fe596 100644 --- a/config/auth.js +++ b/config/auth.js @@ -4,14 +4,45 @@ export default { enabled: true }, jwt: { - enabled: false, + enabled: true, tokenExpiresIn: '1m', refreshTokenExpiresIn: '7d' }, password: { - requireEmailConfirmation: false, - sendPasswordChangesEmail: false, + requireEmailConfirmation: true, + sendPasswordChangesEmail: true, minLength: 8, enabled: true + }, + social: { + facebook: { + enabled: false, + clientID: process.env.FACEBOOK_CLIENTID, + clientSecret: process.env.FACEBOOK_CLIENTSECRET, + callbackURL: '/auth/facebook/callback', + scope: ['email'], + profileFields: ['id', 'emails', 'displayName'] + }, + github: { + enabled: false, + clientID: process.env.GITHUB_CLIENTID, + clientSecret: process.env.GITHUB_CLIENTSECRET, + callbackURL: '/auth/github/callback', + scope: ['user:email'] + }, + linkedin: { + enabled: false, + clientID: process.env.LINKEDIN_CLIENTID, + clientSecret: process.env.LINKEDIN_CLIENTSECRET, + callbackURL: '/auth/linkedin/callback', + scope: ['r_emailaddress', 'r_basicprofile'] + }, + google: { + enabled: false, + clientID: process.env.GOOGLE_CLIENTID, + clientSecret: process.env.GOOGLE_CLIENTSECRET, + callbackURL: '/auth/google/callback', + scope: ['https://www.googleapis.com/auth/userinfo.email', 'https://www.googleapis.com/auth/userinfo.profile'] + } } }; diff --git a/modules/authentication/client-react/access/AccessModule.ts b/modules/authentication/client-react/access/AccessModule.ts new file mode 100644 index 0000000..03d412c --- /dev/null +++ b/modules/authentication/client-react/access/AccessModule.ts @@ -0,0 +1,49 @@ +import ClientModule, { ClientModuleShape } from '@restapp/module-client-react'; + +/** + * Access module which provides authentication functions + */ +export interface AccessModuleShape extends ClientModuleShape { + // login handlers from all authentication modules + login?: Array<(callback?: () => void) => Promise>; + // logout handlers from all authentication modules + logout?: Array<(callback?: () => void) => Promise>; +} + +interface AccessModule extends AccessModuleShape {} + +class AccessModule extends ClientModule { + /** + * Constructs access module representation, that folds all the feature modules + * into a single module represented by this instance. + * + * @param modules feature modules + */ + constructor(...modules: AccessModuleShape[]) { + super(...modules); + } + + /** + * call all methods of login in authentication modules. + * + * @returns merge login methods + */ + public async doLogin(callback?: () => void) { + for (const login of this.login) { + await login(callback); + } + } + + /** + * call all methods of logout in authentication modules. + * + * @returns merge logout methods + */ + public async doLogout(callback?: () => void) { + for (const logout of this.logout) { + await logout(callback); + } + } +} + +export default AccessModule; diff --git a/modules/authentication/client-react/access/index.tsx b/modules/authentication/client-react/access/index.tsx new file mode 100644 index 0000000..5159469 --- /dev/null +++ b/modules/authentication/client-react/access/index.tsx @@ -0,0 +1,48 @@ +import * as React from 'react'; +import jwt from './jwt'; +import session from './session'; +import AccessModule from './AccessModule'; + +interface PageReloaderProps { + children?: React.ReactElement; +} + +interface AuthPageReloaderProps extends PageReloaderProps {} + +const ref: React.RefObject = React.createRef(); + +const rerenderApp = () => { + ref.current.reloadPage(); +}; + +const login = async (clearStore: () => void) => { + await clearStore(); + rerenderApp(); +}; + +const logout = async (clearStore: () => void) => { + await clearStore(); + rerenderApp(); +}; + +class PageReloader extends React.Component { + public state = { + key: 1 + }; + + public reloadPage() { + this.setState({ key: this.state.key + 1 }); + } + + public render() { + return React.cloneElement(this.props.children, { key: this.state.key }); + } +} + +const AuthPageReloader = ({ children }: AuthPageReloaderProps) => {children}; + +export default new AccessModule(jwt, session, { + dataRootComponent: [AuthPageReloader], + login: [login], + logout: [logout] +}); diff --git a/modules/authentication/client-react/access/jwt/index.ts b/modules/authentication/client-react/access/jwt/index.ts new file mode 100644 index 0000000..69a5080 --- /dev/null +++ b/modules/authentication/client-react/access/jwt/index.ts @@ -0,0 +1,86 @@ +import axios from 'axios'; +import { getItem, setItem, removeItem } from '@restapp/core-common/clientStorage'; +import settings from '../../../../../settings'; +import AccessModule from '../AccessModule'; + +enum TokensEnum { + accessToken = 'accessToken', + refreshToken = 'refreshToken' +} + +interface Tokens { + [key: string]: TokensEnum; +} + +const saveTokens = async ({ accessToken, refreshToken }: Tokens) => { + await setItem(TokensEnum.accessToken, accessToken); + await setItem(TokensEnum.refreshToken, refreshToken); +}; + +const removeTokens = async () => { + await removeItem(TokensEnum.accessToken); + await removeItem(TokensEnum.refreshToken); +}; + +axios.interceptors.request.use(async config => { + const accessToken = await getItem(TokensEnum.accessToken); + + if (config.url.includes('currentUser') && !(await getItem(TokensEnum.refreshToken))) { + throw new axios.Cancel('Operation canceled'); + } + + const arrayExceptions = ['login', 'refreshTokens']; + const checkInclude = arrayExceptions.some(exception => config.url.includes(exception)); + + config.headers = !checkInclude && accessToken ? { Authorization: `Bearer ${accessToken}` } : {}; + + return config; +}); + +axios.interceptors.response.use( + async (res: any) => { + if (res.config.url.includes('login')) { + if (!!res.data && res.data.login.tokens) { + const { + data: { + login: { + tokens: { accessToken, refreshToken } + } + } + } = res; + await saveTokens({ accessToken, refreshToken }); + } else { + await removeTokens(); + } + return res; + } + + if (res && res.status > 400 && res.status < 500) { + try { + const { data } = await axios.post(`${__API_URL__}/refreshToken`, { + refreshToken: await getItem('refreshToken') + }); + if (data && data.refreshTokens) { + const { accessToken, refreshToken } = data.refreshTokens; + await saveTokens({ accessToken, refreshToken }); + } else { + await removeTokens(); + } + } catch (e) { + await removeTokens(); + throw e; + } + await res.request(); + } + return res; + }, + err => { + return Promise.reject(err); + } +); + +export default (settings.auth.jwt.enabled + ? new AccessModule({ + logout: [removeTokens] + }) + : undefined); diff --git a/modules/authentication/client-react/access/session/index.ts b/modules/authentication/client-react/access/session/index.ts new file mode 100644 index 0000000..47a5955 --- /dev/null +++ b/modules/authentication/client-react/access/session/index.ts @@ -0,0 +1,13 @@ +import settings from '../../../../../settings'; +import AccessModule from '../AccessModule'; +import axios from 'axios'; + +const logout = async () => { + await axios.post(`${__API_URL__}/logout`); +}; + +export default (settings.auth.session.enabled + ? new AccessModule({ + logout: [logout] + }) + : undefined); diff --git a/modules/authentication/client-react/helpers/index.ts b/modules/authentication/client-react/helpers/index.ts new file mode 100644 index 0000000..3788dba --- /dev/null +++ b/modules/authentication/client-react/helpers/index.ts @@ -0,0 +1,12 @@ +import url from 'url'; +import { Constants } from 'expo'; + +export default function buildRedirectUrlForMobile(authType: string) { + const { protocol, hostname, port } = url.parse(__WEBSITE_URL__); + const expoHostname = `${url.parse(Constants.linkingUri).hostname}.nip.io`; + const urlHostname = process.env.NODE_ENV === 'production' ? hostname : expoHostname; + + return `${protocol}//${urlHostname}${port ? ':' + port : ''}/auth/${authType}?expoUrl=${encodeURIComponent( + Constants.linkingUri + )}`; +} diff --git a/modules/authentication/client-react/index.ts b/modules/authentication/client-react/index.ts new file mode 100644 index 0000000..e74ec5e --- /dev/null +++ b/modules/authentication/client-react/index.ts @@ -0,0 +1,5 @@ +import authentication from './access'; + +export * from './social'; + +export default authentication; diff --git a/modules/authentication/client-react/package.json b/modules/authentication/client-react/package.json new file mode 100644 index 0000000..5502094 --- /dev/null +++ b/modules/authentication/client-react/package.json @@ -0,0 +1,8 @@ +{ + "name": "@restapp/authentication-client-react", + "version": "0.1.0", + "private": true, + "dependencies": { + "@restapp/core-common": "^0.1.0" + } +} \ No newline at end of file diff --git a/modules/authentication/client-react/social/facebook/containers/FacebookButton.css b/modules/authentication/client-react/social/facebook/containers/FacebookButton.css new file mode 100644 index 0000000..f7a0a10 --- /dev/null +++ b/modules/authentication/client-react/social/facebook/containers/FacebookButton.css @@ -0,0 +1,39 @@ +.facebookBtn { + min-width: 320px; + margin-top: 10px; + background-color: #3769ae; + border-color: #3769ae; + display: flex; + justify-content: flex-start; + align-items: center; +} + +.facebookBtn:hover { + background-color: #17427e; + border-color: #17427e; +} + +.iconContainer { + display: flex; + flex: 1 1 10%; + justify-content: flex-start; + align-items: center; +} + +.separator { + height: 28px; + width: 1px; + background-color: #fff !important; + margin-left: 10px; +} + +.btnText { + display: flex; + justify-content: flex-start; + flex: 5 +} + +.facebookIcon { + color: #fff; + font-size: 30px; +} diff --git a/modules/authentication/client-react/social/facebook/containers/FacebookButton.native.tsx b/modules/authentication/client-react/social/facebook/containers/FacebookButton.native.tsx new file mode 100644 index 0000000..1dca6f6 --- /dev/null +++ b/modules/authentication/client-react/social/facebook/containers/FacebookButton.native.tsx @@ -0,0 +1,80 @@ +import React from 'react'; +import { View, StyleSheet, Linking, TouchableOpacity, Text, Platform } from 'react-native'; +import { WebBrowser } from 'expo'; +import { FontAwesome } from '@expo/vector-icons'; +import { + iconWrapper, + linkText, + link, + buttonContainer, + separator, + btnIconContainer, + btnTextContainer, + btnText +} from '@restapp/look-client-react-native/styles'; + +import { SocialButton, SocialButtonComponent } from '../../..'; +import buildRedirectUrlForMobile from '../../../helpers'; + +const facebookLogin = () => { + const url = buildRedirectUrlForMobile('facebook'); + if (Platform.OS === 'ios') { + WebBrowser.openBrowserAsync(url); + } else { + Linking.openURL(url); + } +}; + +const FacebookButton = ({ text }: SocialButtonComponent) => { + return ( + + + + + + + {text} + + + ); +}; + +const FacebookLink = ({ text }: SocialButtonComponent) => { + return ( + + {text} + + ); +}; + +const FacebookIcon = () => ( + + + +); + +const FacebookComponent: React.FunctionComponent = ({ type, text }) => { + switch (type) { + case 'button': + return ; + case 'link': + return ; + case 'icon': + return ; + default: + return ; + } +}; + +const styles = StyleSheet.create({ + iconWrapper, + linkText, + link, + buttonContainer, + separator, + btnIconContainer, + btnTextContainer, + btnText +}); + +export default FacebookComponent; diff --git a/modules/authentication/client-react/social/facebook/containers/FacebookButton.tsx b/modules/authentication/client-react/social/facebook/containers/FacebookButton.tsx new file mode 100644 index 0000000..5d8ada5 --- /dev/null +++ b/modules/authentication/client-react/social/facebook/containers/FacebookButton.tsx @@ -0,0 +1,55 @@ +import * as React from 'react'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faFacebookSquare } from '@fortawesome/fontawesome-free-brands'; +import { IconProp } from '@fortawesome/fontawesome-svg-core'; +import { Button } from '@restapp/look-client-react'; + +import { SocialButton, SocialButtonComponent } from '../../..'; +import './FacebookButton.css'; + +const facebookLogin = () => { + window.location.href = '/auth/facebook'; +}; + +const FacebookButton = ({ text }: SocialButtonComponent) => { + return ( + + ); +}; + +const FacebookLink = ({ text }: SocialButtonComponent) => { + return ( + + ); +}; + +const FacebookIcon = () => ( +

+ +
+); + +const FacebookComponent: React.FunctionComponent = ({ text, type }) => { + switch (type) { + case 'button': + return ; + case 'link': + return ; + case 'icon': + return ; + default: + return ; + } +}; + +export default FacebookComponent; diff --git a/modules/authentication/client-react/social/facebook/index.native.ts b/modules/authentication/client-react/social/facebook/index.native.ts new file mode 100644 index 0000000..d22328c --- /dev/null +++ b/modules/authentication/client-react/social/facebook/index.native.ts @@ -0,0 +1,3 @@ +import FacebookButton from './containers/FacebookButton'; + +export default FacebookButton; diff --git a/modules/authentication/client-react/social/facebook/index.ts b/modules/authentication/client-react/social/facebook/index.ts new file mode 100644 index 0000000..d22328c --- /dev/null +++ b/modules/authentication/client-react/social/facebook/index.ts @@ -0,0 +1,3 @@ +import FacebookButton from './containers/FacebookButton'; + +export default FacebookButton; diff --git a/modules/authentication/client-react/social/github/containers/GitHubButton.css b/modules/authentication/client-react/social/github/containers/GitHubButton.css new file mode 100644 index 0000000..3c35726 --- /dev/null +++ b/modules/authentication/client-react/social/github/containers/GitHubButton.css @@ -0,0 +1,39 @@ +.githubBtn { + min-width: 320px; + margin-top: 10px; + background-color: #464646; + border-color: #464646; + display: flex; + justify-content: flex-start; + align-items: center; +} + +.githubBtn:hover { + background-color: #5f5e5e; + border-color: #5f5e5e; +} + +.iconContainer { + display: flex; + flex: 1 1 10%; + justify-content: flex-start; + align-items: center; +} + +.separator { + height: 28px; + width: 1px; + background-color: #fff !important; + margin-left: 10px; +} + +.btnText { + display: flex; + justify-content: flex-start; + flex: 5 +} + +.githubIcon { + color: #fff; + font-size: 30px; +} \ No newline at end of file diff --git a/modules/authentication/client-react/social/github/containers/GitHubButton.native.tsx b/modules/authentication/client-react/social/github/containers/GitHubButton.native.tsx new file mode 100644 index 0000000..80b78e0 --- /dev/null +++ b/modules/authentication/client-react/social/github/containers/GitHubButton.native.tsx @@ -0,0 +1,84 @@ +import React from 'react'; +import { View, StyleSheet, Linking, TouchableOpacity, Text, Platform } from 'react-native'; +import { WebBrowser } from 'expo'; +import { FontAwesome } from '@expo/vector-icons'; +import { + iconWrapper, + linkText, + link, + buttonContainer, + separator, + btnIconContainer, + btnTextContainer, + btnText +} from '@restapp/look-client-react-native/styles'; + +import { SocialButton, SocialButtonComponent } from '../..'; +import buildRedirectUrlForMobile from '../../../helpers'; + +const githubLogin = () => { + const url = buildRedirectUrlForMobile('github'); + if (Platform.OS === 'ios') { + WebBrowser.openBrowserAsync(url); + } else { + Linking.openURL(url); + } +}; + +const GitHubButton = ({ text }: SocialButtonComponent) => { + return ( + + + + + + + {text} + + + ); +}; + +const GitHubLink = ({ text }: SocialButtonComponent) => { + return ( + + {text} + + ); +}; + +const GitHubIcon = () => ( + + + +); + +const GitHubComponent: React.FunctionComponent = ({ type, text }) => { + switch (type) { + case 'button': + return ; + case 'link': + return ; + case 'icon': + return ; + default: + return ; + } +}; + +const styles = StyleSheet.create({ + iconWrapper, + linkText, + link, + buttonContainer: { + ...buttonContainer, + marginTop: 15, + backgroundColor: '#464646' + }, + separator, + btnIconContainer, + btnTextContainer, + btnText +}); + +export default GitHubComponent; diff --git a/modules/authentication/client-react/social/github/containers/GitHubButton.tsx b/modules/authentication/client-react/social/github/containers/GitHubButton.tsx new file mode 100644 index 0000000..a459525 --- /dev/null +++ b/modules/authentication/client-react/social/github/containers/GitHubButton.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faGithubSquare } from '@fortawesome/fontawesome-free-brands'; +import { IconProp } from '@fortawesome/fontawesome-svg-core'; +import { Button } from '@restapp/look-client-react'; + +import { SocialButton, SocialButtonComponent } from '../..'; +import './GitHubButton.css'; + +const githubLogin = () => { + window.location.href = '/auth/github'; +}; + +const GitHubButton = ({ text }: SocialButtonComponent) => { + return ( + + ); +}; + +const GitHubLink = ({ text }: SocialButtonComponent) => { + return ( + + ); +}; + +const GitHubIcon = () => ( +
+ +
+); + +const GithubComponent: React.FunctionComponent = ({ text, type }) => { + switch (type) { + case 'button': + return ; + case 'link': + return ; + case 'icon': + return ; + default: + return ; + } +}; + +export default GithubComponent; diff --git a/modules/authentication/client-react/social/github/index.native.ts b/modules/authentication/client-react/social/github/index.native.ts new file mode 100644 index 0000000..2787227 --- /dev/null +++ b/modules/authentication/client-react/social/github/index.native.ts @@ -0,0 +1,3 @@ +import GitHubButton from './containers/GitHubButton'; + +export default GitHubButton; diff --git a/modules/authentication/client-react/social/github/index.ts b/modules/authentication/client-react/social/github/index.ts new file mode 100644 index 0000000..2787227 --- /dev/null +++ b/modules/authentication/client-react/social/github/index.ts @@ -0,0 +1,3 @@ +import GitHubButton from './containers/GitHubButton'; + +export default GitHubButton; diff --git a/modules/authentication/client-react/social/google/containers/GoogleButton.css b/modules/authentication/client-react/social/google/containers/GoogleButton.css new file mode 100644 index 0000000..236eb58 --- /dev/null +++ b/modules/authentication/client-react/social/google/containers/GoogleButton.css @@ -0,0 +1,39 @@ +.googleBtn { + min-width: 320px; + margin-top: 10px; + background-color: #c43832; + border-color: #c43832; + display: flex; + justify-content: flex-start; + align-items: center; +} + +.googleBtn:hover { + background-color: #aa1c17; + border-color: #aa1c17; +} + +.iconContainer { + display: flex; + flex: 1 1 10%; + justify-content: flex-start; + align-items: center; +} + +.separator { + height: 28px; + width: 1px; + background-color: #fff; + margin-left: 10px; +} + +.btnText { + display: flex; + justify-content: flex-start; + flex: 5 +} + +.googleIcon { + color: #fff; + font-size: 30px; +} \ No newline at end of file diff --git a/modules/authentication/client-react/social/google/containers/GoogleButton.native.tsx b/modules/authentication/client-react/social/google/containers/GoogleButton.native.tsx new file mode 100644 index 0000000..d448b1c --- /dev/null +++ b/modules/authentication/client-react/social/google/containers/GoogleButton.native.tsx @@ -0,0 +1,87 @@ +import React from 'react'; +import { View, StyleSheet, Linking, TouchableOpacity, Text, Platform } from 'react-native'; +import { WebBrowser } from 'expo'; +import { FontAwesome } from '@expo/vector-icons'; +import { + iconWrapper, + linkText, + link, + buttonContainer, + separator, + btnIconContainer, + btnTextContainer, + btnText +} from '@restapp/look-client-react-native/styles'; + +import { SocialButtonComponent, SocialButton } from '../..'; +import buildRedirectUrlForMobile from '../../../helpers'; + +const googleLogin = () => { + const url = buildRedirectUrlForMobile('google'); + if (Platform.OS === 'ios') { + WebBrowser.openBrowserAsync(url); + } else { + Linking.openURL(url); + } +}; + +const GoogleButton = ({ text }: SocialButtonComponent) => { + return ( + + + + + + + {text} + + + ); +}; + +const GoogleLink = ({ text }: SocialButtonComponent) => { + return ( + + {text} + + ); +}; + +const GoogleIcon = () => ( + + + +); + +const GoogleComponent: React.FunctionComponent = ({ type, text }) => { + switch (type) { + case 'button': + return ; + case 'link': + return ; + case 'icon': + return ; + default: + return ; + } +}; + +const styles = StyleSheet.create({ + iconWrapper, + linkText, + link, + buttonContainer: { + ...buttonContainer, + marginTop: 15, + backgroundColor: '#c43832' + }, + separator: { + ...separator, + backgroundColor: '#fff' + }, + btnIconContainer, + btnTextContainer, + btnText +}); + +export default GoogleComponent; diff --git a/modules/authentication/client-react/social/google/containers/GoogleButton.tsx b/modules/authentication/client-react/social/google/containers/GoogleButton.tsx new file mode 100644 index 0000000..168482c --- /dev/null +++ b/modules/authentication/client-react/social/google/containers/GoogleButton.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import faGooglePlusSquare from '@fortawesome/fontawesome-free-brands/faGooglePlusSquare'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { IconProp } from '@fortawesome/fontawesome-svg-core'; +import { Button } from '@restapp/look-client-react'; + +import { SocialButtonComponent, SocialButton } from '../..'; +import './GoogleButton.css'; + +const googleLogin = () => { + window.location.href = '/auth/google'; +}; + +const GoogleButton = ({ text }: SocialButtonComponent) => { + return ( + + ); +}; + +const GoogleLink = ({ text }: SocialButtonComponent) => { + return ( + + ); +}; + +const GoogleIcon = () => ( +
+ +
+); + +const GoogleComponent: React.FunctionComponent = ({ type, text }) => { + switch (type) { + case 'button': + return ; + case 'link': + return ; + case 'icon': + return ; + default: + return ; + } +}; + +export default GoogleComponent; diff --git a/modules/authentication/client-react/social/google/index.native.ts b/modules/authentication/client-react/social/google/index.native.ts new file mode 100644 index 0000000..bea8dc6 --- /dev/null +++ b/modules/authentication/client-react/social/google/index.native.ts @@ -0,0 +1,3 @@ +import GoogleButton from './containers/GoogleButton'; + +export default GoogleButton; diff --git a/modules/authentication/client-react/social/google/index.ts b/modules/authentication/client-react/social/google/index.ts new file mode 100644 index 0000000..bea8dc6 --- /dev/null +++ b/modules/authentication/client-react/social/google/index.ts @@ -0,0 +1,3 @@ +import GoogleButton from './containers/GoogleButton'; + +export default GoogleButton; diff --git a/modules/authentication/client-react/social/index.ts b/modules/authentication/client-react/social/index.ts new file mode 100644 index 0000000..15e0f59 --- /dev/null +++ b/modules/authentication/client-react/social/index.ts @@ -0,0 +1,12 @@ +export interface SocialButtonComponent { + text?: string; +} + +export interface SocialButton extends SocialButtonComponent { + type: string; +} + +export { default as LinkedInButton } from './linkedin'; +export { default as GoogleButton } from './google'; +export { default as GitHubButton } from './github'; +export { default as FacebookButton } from './facebook'; diff --git a/modules/authentication/client-react/social/linkedin/containers/LinkedInButton.css b/modules/authentication/client-react/social/linkedin/containers/LinkedInButton.css new file mode 100644 index 0000000..f044014 --- /dev/null +++ b/modules/authentication/client-react/social/linkedin/containers/LinkedInButton.css @@ -0,0 +1,39 @@ +.linkedInBtn { + min-width: 320px; + margin-top: 10px; + background-color: #0077b0; + border-color: #0077b0; + display: flex; + justify-content: flex-start; + align-items: center; +} + +.linkedInBtn:hover { + background-color: #054b6b; + border-color: #054b6b; +} + +.iconContainer { + display: flex; + flex: 1 1 10%; + justify-content: flex-start; + align-items: center; +} + +.separator { + height: 28px; + width: 1px; + background-color: #fff !important; + margin-left: 10px; +} + +.btnText { + display: flex; + justify-content: flex-start; + flex: 5 +} + +.linkedInIcon { + color: #fff; + font-size: 30px; +} diff --git a/modules/authentication/client-react/social/linkedin/containers/LinkedInButton.native.tsx b/modules/authentication/client-react/social/linkedin/containers/LinkedInButton.native.tsx new file mode 100644 index 0000000..dcf13f5 --- /dev/null +++ b/modules/authentication/client-react/social/linkedin/containers/LinkedInButton.native.tsx @@ -0,0 +1,84 @@ +import React from 'react'; +import { View, StyleSheet, Linking, TouchableOpacity, Text, Platform } from 'react-native'; +import { WebBrowser } from 'expo'; +import { FontAwesome } from '@expo/vector-icons'; +import { + iconWrapper, + linkText, + link, + buttonContainer, + separator, + btnIconContainer, + btnTextContainer, + btnText +} from '@restapp/look-client-react-native/styles'; + +import { SocialButton, SocialButtonComponent } from '../..'; +import buildRedirectUrlForMobile from '../../../helpers'; + +const linkedInLogin = () => { + const url = buildRedirectUrlForMobile('linkedin'); + if (Platform.OS === 'ios') { + WebBrowser.openBrowserAsync(url); + } else { + Linking.openURL(url); + } +}; + +const LinkedInButton = ({ text }: SocialButtonComponent) => { + return ( + + + + + + + {text} + + + ); +}; + +const LinkedInLink = ({ text }: SocialButtonComponent) => { + return ( + + {text} + + ); +}; + +const LinkedInIcon = () => ( + + + +); + +const LinkedInComponent: React.FunctionComponent = ({ type, text }) => { + switch (type) { + case 'button': + return ; + case 'link': + return ; + case 'icon': + return ; + default: + return ; + } +}; + +const styles = StyleSheet.create({ + iconWrapper, + linkText, + link, + buttonContainer: { + ...buttonContainer, + marginTop: 15, + backgroundColor: '#0077b0' + }, + separator, + btnIconContainer, + btnTextContainer, + btnText +}); + +export default LinkedInComponent; diff --git a/modules/authentication/client-react/social/linkedin/containers/LinkedInButton.tsx b/modules/authentication/client-react/social/linkedin/containers/LinkedInButton.tsx new file mode 100644 index 0000000..696c6eb --- /dev/null +++ b/modules/authentication/client-react/social/linkedin/containers/LinkedInButton.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { faLinkedin } from '@fortawesome/fontawesome-free-brands'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { IconProp } from '@fortawesome/fontawesome-svg-core'; +import { Button } from '@restapp/look-client-react'; + +import { SocialButtonComponent, SocialButton } from '../..'; +import './LinkedInButton.css'; + +const linkedInLogin = () => { + window.location.href = '/auth/linkedin'; +}; + +const LinkedInButton = ({ text }: SocialButtonComponent) => { + return ( + + ); +}; + +const LinkedInLink = ({ text }: SocialButtonComponent) => { + return ( + + ); +}; + +const LinkedInIcon = () => ( +
+ +
+); + +const LinkedInComponent: React.FunctionComponent = ({ text, type }) => { + switch (type) { + case 'button': + return ; + case 'link': + return ; + case 'icon': + return ; + default: + return ; + } +}; + +export default LinkedInComponent; diff --git a/modules/authentication/client-react/social/linkedin/index.native.ts b/modules/authentication/client-react/social/linkedin/index.native.ts new file mode 100644 index 0000000..8d6a494 --- /dev/null +++ b/modules/authentication/client-react/social/linkedin/index.native.ts @@ -0,0 +1,3 @@ +import LinkedInButton from './containers/LinkedInButton'; + +export default LinkedInButton; diff --git a/modules/authentication/client-react/social/linkedin/index.ts b/modules/authentication/client-react/social/linkedin/index.ts new file mode 100644 index 0000000..8d6a494 --- /dev/null +++ b/modules/authentication/client-react/social/linkedin/index.ts @@ -0,0 +1,3 @@ +import LinkedInButton from './containers/LinkedInButton'; + +export default LinkedInButton; diff --git a/modules/look/client-react-native/styles/button.js b/modules/look/client-react-native/styles/button.js deleted file mode 100644 index e3f09e7..0000000 --- a/modules/look/client-react-native/styles/button.js +++ /dev/null @@ -1,5 +0,0 @@ -// eslint-disable-next-line -export const submit = { - paddingTop: 30, - paddingBottom: 15 -}; diff --git a/modules/look/client-react-native/styles/button.ts b/modules/look/client-react-native/styles/button.ts new file mode 100644 index 0000000..565dae2 --- /dev/null +++ b/modules/look/client-react-native/styles/button.ts @@ -0,0 +1,6 @@ +import { ViewStyle } from 'react-native'; + +export const submit: ViewStyle = { + paddingTop: 30, + paddingBottom: 15 +}; diff --git a/modules/look/client-react-native/styles/colors.js b/modules/look/client-react-native/styles/colors.ts similarity index 100% rename from modules/look/client-react-native/styles/colors.js rename to modules/look/client-react-native/styles/colors.ts diff --git a/modules/look/client-react-native/styles/index.js b/modules/look/client-react-native/styles/index.ts similarity index 100% rename from modules/look/client-react-native/styles/index.js rename to modules/look/client-react-native/styles/index.ts diff --git a/modules/look/client-react-native/styles/itemContainer.js b/modules/look/client-react-native/styles/itemContainer.ts similarity index 52% rename from modules/look/client-react-native/styles/itemContainer.js rename to modules/look/client-react-native/styles/itemContainer.ts index 7f8f4ee..8c03378 100644 --- a/modules/look/client-react-native/styles/itemContainer.js +++ b/modules/look/client-react-native/styles/itemContainer.ts @@ -1,15 +1,17 @@ -export const itemContainer = { +import { ViewStyle, TextStyle } from 'react-native'; + +export const itemContainer: ViewStyle = { flex: 1, flexDirection: 'row', alignItems: 'center' }; -export const itemTitle = { +export const itemTitle: TextStyle = { flex: 5, flexDirection: 'column' }; -export const itemAction = { +export const itemAction: ViewStyle = { flex: 1, flexDirection: 'column', justifyContent: 'center', diff --git a/modules/look/client-react-native/styles/socialButton.js b/modules/look/client-react-native/styles/socialButton.ts similarity index 68% rename from modules/look/client-react-native/styles/socialButton.js rename to modules/look/client-react-native/styles/socialButton.ts index 83393bf..2afa9d4 100644 --- a/modules/look/client-react-native/styles/socialButton.js +++ b/modules/look/client-react-native/styles/socialButton.ts @@ -1,45 +1,47 @@ -const iconWrapper = { +import { ViewStyle, TextStyle } from 'react-native'; + +const iconWrapper: ViewStyle = { alignItems: 'center', marginTop: 10 }; -const linkText = { +const linkText: TextStyle = { color: '#0056b3', fontSize: 16, fontWeight: '600' }; -const link = { +const link: ViewStyle = { justifyContent: 'center', alignItems: 'center', margin: 10 }; -const buttonContainer = { +const buttonContainer: ViewStyle = { height: 45, flexDirection: 'row', alignItems: 'center', backgroundColor: '#3769ae', borderRadius: 4 }; -const separator = { +const separator: ViewStyle = { height: 30, width: 1.5, marginLeft: 10, backgroundColor: '#fff' }; -const btnIconContainer = { +const btnIconContainer: ViewStyle = { flex: 2, justifyContent: 'flex-start', alignItems: 'center', flexDirection: 'row' }; -const btnTextContainer = { +const btnTextContainer: ViewStyle = { flex: 5, justifyContent: 'center', alignItems: 'flex-start' }; -const btnText = { +const btnText: TextStyle = { color: '#fff', fontSize: 16, fontWeight: '400' diff --git a/packages/client/package.json b/packages/client/package.json index d69a9ac..8375cb5 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -40,15 +40,19 @@ "dependencies": { "@fortawesome/fontawesome": "^1.1.8", "@fortawesome/fontawesome-free-brands": "^5.0.6", - "@fortawesome/react-fontawesome": "0.0.18", + "@fortawesome/fontawesome-svg-core": "^1.2.10", + "@fortawesome/react-fontawesome": "^0.1.3", "@restapp/core-client-react": "^0.1.0", "@restapp/forms-client-react": "^0.1.0", "@restapp/i18n-client-react": "^0.1.0", "@restapp/i18n-common-react": "^0.1.0", "@restapp/module-client-react": "^0.1.0", + "@restapp/user-client-react": "^0.1.0", + "@restapp/authentication-client-react": "^0.1.0", "@restapp/validation-common-react": "^0.1.0", "@restapp/welcome-client-react": "^0.1.0", "antd": "^3.10.0", + "axios": "^0.18.0", "babel-polyfill": "^6.26.0", "bootstrap": "^4.1.1", "error-stack-parser": "^2.0.1", @@ -147,4 +151,4 @@ "not ie < 11" ] } -} +} \ No newline at end of file diff --git a/packages/client/src/modules.ts b/packages/client/src/modules.ts index 3e3038d..828b088 100644 --- a/packages/client/src/modules.ts +++ b/packages/client/src/modules.ts @@ -1,5 +1,6 @@ import welcome from '@restapp/welcome-client-react'; import user from '@restapp/user-client-react'; +import authentication from '@restapp/authentication-client-react'; import core from '@restapp/core-client-react'; import look from '@restapp/look-client-react'; import i18n from '@restapp/i18n-client-react'; @@ -10,6 +11,16 @@ import '@restapp/favicon-common'; const pageNotFound = require('@restapp/page-not-found-client-react').default; -const modules = new ClientModule(user, welcome, look, validation, defaultRouter, i18n, pageNotFound, core); +const modules = new ClientModule( + authentication, + user, + welcome, + look, + validation, + defaultRouter, + i18n, + pageNotFound, + core +); export default modules; diff --git a/packages/mobile/src/modules.ts b/packages/mobile/src/modules.ts index 09a076e..2523612 100644 --- a/packages/mobile/src/modules.ts +++ b/packages/mobile/src/modules.ts @@ -1,5 +1,6 @@ import welcome from '@restapp/welcome-client-react'; import user from '@restapp/user-client-react'; +import authentication from '@restapp/authentication-client-react'; import core from '@restapp/core-client-react-native'; import i18n from '@restapp/i18n-client-react'; import validation from '@restapp/validation-common-react'; @@ -7,6 +8,6 @@ import defaultRouter from '@restapp/router-client-react-native'; import ClientModule from '@restapp/module-client-react-native'; -const modules = new ClientModule(user, welcome, validation, defaultRouter, i18n, core); +const modules = new ClientModule(defaultRouter, authentication, user, welcome, validation, i18n, core); export default modules; diff --git a/yarn.lock b/yarn.lock index 9025673..6a469e9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1170,6 +1170,11 @@ resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.1.7.tgz#4336c4b06d0b5608ff1215464b66fcf9f4795284" integrity sha512-ego8jRVSHfq/iq4KRZJKQeUAdi3ZjGNrqw4oPN3fNdvTBnLCSntwVCnc37bsAJP9UB8MhrTfPnZYxkv2vpS4pg== +"@fortawesome/fontawesome-common-types@^0.2.17": + version "0.2.17" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.17.tgz#d8c36e6f6f3b3415fa1f83eaffe4f41bd313715c" + integrity sha512-DEYsEb/iiGVoMPQGjhG2uOylLVuMzTxOxysClkabZ5n80Q3oFDWGnshCLKvOvKoeClsgmKhWVrnnqvsMI1cAbw== + "@fortawesome/fontawesome-free-brands@^5.0.6": version "5.0.13" resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free-brands/-/fontawesome-free-brands-5.0.13.tgz#4d15ff4e1e862d5e4a4df3654f8e8acbd47e9c09" @@ -1177,6 +1182,13 @@ dependencies: "@fortawesome/fontawesome-common-types" "^0.1.7" +"@fortawesome/fontawesome-svg-core@^1.2.10": + version "1.2.17" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.17.tgz#8fce4402e824ebe99a04b1949d56d696eeae2e6d" + integrity sha512-TORMW/wIX2QyyGBd4XwHGPir4/0U18Wxf+iDBAUW3EIJ0/VC/ZMpJOiyiCe1f8g9h0PPzA7sqVtl8JtTUtm4uA== + dependencies: + "@fortawesome/fontawesome-common-types" "^0.2.17" + "@fortawesome/fontawesome@^1.1.8": version "1.1.8" resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome/-/fontawesome-1.1.8.tgz#75fe66a60f95508160bb16bd781ad7d89b280f5b" @@ -1184,12 +1196,13 @@ dependencies: "@fortawesome/fontawesome-common-types" "^0.1.7" -"@fortawesome/react-fontawesome@0.0.18": - version "0.0.18" - resolved "https://registry.yarnpkg.com/@fortawesome/react-fontawesome/-/react-fontawesome-0.0.18.tgz#4e0eb1cf9797715a67bb7705ae084fa0a410f185" - integrity sha512-ZSO55oWtGSZFvoJW0gvhlYndKvHqmMj/27qTrJOT4DaxQ4Fcc90h/BVEED6IktzBqEunUF8aP+bwU5Og1I0fnQ== +"@fortawesome/react-fontawesome@^0.1.3": + version "0.1.4" + resolved "https://registry.yarnpkg.com/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.4.tgz#18d61d9b583ca289a61aa7dccc05bd164d6bc9ad" + integrity sha512-GwmxQ+TK7PEdfSwvxtGnMCqrfEm0/HbRHArbUudsYiy9KzVCwndxa2KMcfyTQ8El0vROrq8gOOff09RF1oQe8g== dependencies: humps "^2.0.1" + prop-types "^15.5.10" "@jest/types@^24.5.0": version "24.5.0" @@ -3386,6 +3399,14 @@ axios@0.16.2: follow-redirects "^1.2.3" is-buffer "^1.1.5" +axios@^0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.18.0.tgz#32d53e4851efdc0a11993b6cd000789d70c05102" + integrity sha1-MtU+SFHv3AoRmTts0AB4nXDAUQI= + dependencies: + follow-redirects "^1.3.0" + is-buffer "^1.1.5" + axios@v0.19.0-beta.1: version "0.19.0-beta.1" resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.0-beta.1.tgz#3d6a9ee75885d1fd39e108df9a4fb2e48e1af1e8" @@ -8106,7 +8127,7 @@ flush-write-stream@^1.0.0: inherits "^2.0.3" readable-stream "^2.3.6" -follow-redirects@^1.0.0, follow-redirects@^1.2.3, follow-redirects@^1.4.1: +follow-redirects@^1.0.0, follow-redirects@^1.2.3, follow-redirects@^1.3.0, follow-redirects@^1.4.1: version "1.7.0" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.7.0.tgz#489ebc198dc0e7f64167bd23b03c4c19b5784c76" integrity sha512-m/pZQy4Gj287eNy94nivy5wchN3Kp+Q5WgUPNy5lJSZ3sgkVKSYV/ZChMAQVIgx1SqfZ2zBZtPA2YlXIWxxJOQ== @@ -9305,7 +9326,7 @@ immediate@^3.2.2: resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.2.3.tgz#d140fa8f614659bd6541233097ddaac25cdd991c" integrity sha1-0UD6j2FGWb1lQSMwl92qwlzdmRw= -immutability-helper@^2.6.2: +immutability-helper@2.8.1, immutability-helper@^2.6.2: version "2.8.1" resolved "https://registry.yarnpkg.com/immutability-helper/-/immutability-helper-2.8.1.tgz#3c5ec05fcd83676bfae7146f319595243ad904f4" integrity sha512-8AVB5EUpRBUdXqfe4cFsFECsOIZ9hX/Arl8B8S9/tmwpYv3UWvOsXUPOjkuZIMaVxfSWkxCzkng1rjmEoSWrxQ== @@ -10154,6 +10175,11 @@ items@2.x.x: resolved "https://registry.yarnpkg.com/items/-/items-2.1.2.tgz#0849354595805d586dac98e7e6e85556ea838558" integrity sha512-kezcEqgB97BGeZZYtX/MA8AG410ptURstvnz5RAgyFZ8wQFPMxHY8GpTq+/ZHKT3frSlIthUq7EvLt9xn3TvXg== +iterall@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.2.2.tgz#92d70deb8028e0c39ff3164fdbf4d8b088130cd7" + integrity sha512-yynBb1g+RFUPY64fTrFv7nsjRrENBQJaX2UL+2Szc9REFrSNm1rpSXHGzhmAy7a9uv3vlvgBlXnf9RqmPH1/DA== + jest-changed-files@^23.4.2: version "23.4.2" resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-23.4.2.tgz#1eed688370cd5eebafe4ae93d34bb3b64968fe83" @@ -18509,7 +18535,7 @@ watch@~0.18.0: exec-sh "^0.2.0" minimist "^1.2.0" -watchpack@^1.5.0: +watchpack@^1.5.0, "watchpack@https://github.com/Globegitter/watchpack": version "1.6.0" resolved "https://github.com/Globegitter/watchpack#d903e64a910ab068299f824b04a5e216a15672e9" dependencies: From 4453d8c9225aa50677cd8cd373582395b6d89d5c Mon Sep 17 00:00:00 2001 From: Ivan Isakov Date: Wed, 8 May 2019 12:48:08 +0300 Subject: [PATCH 030/104] Add httpClient in Client module and create request handler --- .../client-react/access/jwt/index.ts | 77 ++++++++++--------- modules/core/client-react/Main.tsx | 2 +- modules/core/common/createReduxStore.ts | 22 ++++-- modules/module/client-react/ClientModule.ts | 9 +++ modules/user/client-react/actions/register.ts | 5 +- 5 files changed, 71 insertions(+), 44 deletions(-) diff --git a/modules/authentication/client-react/access/jwt/index.ts b/modules/authentication/client-react/access/jwt/index.ts index 69a5080..dc9edfc 100644 --- a/modules/authentication/client-react/access/jwt/index.ts +++ b/modules/authentication/client-react/access/jwt/index.ts @@ -22,6 +22,32 @@ const removeTokens = async () => { await removeItem(TokensEnum.refreshToken); }; +const client = async (request: () => Promise) => { + try { + const result = await request(); + return result; + } catch (e) { + if (e.response && e.response.status === 401) { + try { + const { data } = await axios.post(`${__API_URL__}/refreshToken`, { + refreshToken: await getItem('refreshToken') + }); + if (data && data.refreshTokens) { + const { accessToken, refreshToken } = data.refreshTokens; + await saveTokens({ accessToken, refreshToken }); + } else { + await removeTokens(); + } + } catch (e) { + await removeTokens(); + throw e; + } + await request(); + } + return e.response; + } +}; + axios.interceptors.request.use(async config => { const accessToken = await getItem(TokensEnum.accessToken); @@ -37,50 +63,27 @@ axios.interceptors.request.use(async config => { return config; }); -axios.interceptors.response.use( - async (res: any) => { - if (res.config.url.includes('login')) { - if (!!res.data && res.data.login.tokens) { - const { - data: { - login: { - tokens: { accessToken, refreshToken } - } +axios.interceptors.response.use(async (res: any) => { + if (res.config.url.includes('login')) { + if (!!res.data && res.data.login.tokens) { + const { + data: { + login: { + tokens: { accessToken, refreshToken } } - } = res; - await saveTokens({ accessToken, refreshToken }); - } else { - await removeTokens(); - } - return res; - } - - if (res && res.status > 400 && res.status < 500) { - try { - const { data } = await axios.post(`${__API_URL__}/refreshToken`, { - refreshToken: await getItem('refreshToken') - }); - if (data && data.refreshTokens) { - const { accessToken, refreshToken } = data.refreshTokens; - await saveTokens({ accessToken, refreshToken }); - } else { - await removeTokens(); } - } catch (e) { - await removeTokens(); - throw e; - } - await res.request(); + } = res; + await saveTokens({ accessToken, refreshToken }); + } else { + await removeTokens(); } return res; - }, - err => { - return Promise.reject(err); } -); +}); export default (settings.auth.jwt.enabled ? new AccessModule({ - logout: [removeTokens] + logout: [removeTokens], + httpClient: client }) : undefined); diff --git a/modules/core/client-react/Main.tsx b/modules/core/client-react/Main.tsx index 7f0233d..1a5aca9 100644 --- a/modules/core/client-react/Main.tsx +++ b/modules/core/client-react/Main.tsx @@ -25,7 +25,7 @@ export const onAppCreate = (modules: ClientModule, entryModule: NodeModule) => { ref.store = entryModule.hot.data.store; ref.store.replaceReducer(getStoreReducer(ref.modules.reducers)); } else { - ref.store = createReduxStore(ref.modules.reducers, {}, routerMiddleware(history)); + ref.store = createReduxStore(ref.modules.reducers, {}, routerMiddleware(history), ref.modules.httpClients); } }; diff --git a/modules/core/common/createReduxStore.ts b/modules/core/common/createReduxStore.ts index 5bcda53..bd4b566 100644 --- a/modules/core/common/createReduxStore.ts +++ b/modules/core/common/createReduxStore.ts @@ -8,7 +8,9 @@ export const getStoreReducer = (reducers: any) => ...reducers }); -const requestMiddleware: Middleware = _state => next => action => { +const requestMiddleware: ( + httpClient?: (request: () => Promise) => void +) => Middleware = httpClient => _state => next => action => { const { types, callAPI, ...rest } = action; if (!types) { @@ -21,14 +23,17 @@ const requestMiddleware: Middleware = _state => next => action => { const handleCallApi = async () => { try { - const { data } = await callAPI(); + const { data } = await callAPI(httpClient); next({ type: SUCCESS, payload: data, ...rest }); return data; - } catch ({ response: { data } }) { + } catch (e) { + const { + response: { data } + } = e; next({ type: FAIL, payload: data, @@ -41,8 +46,15 @@ const requestMiddleware: Middleware = _state => next => action => { return handleCallApi(); }; -const createReduxStore = (reducers: Reducer, initialState: DeepPartial, routerMiddleware?: Middleware): Store => { - const middleware = routerMiddleware ? [routerMiddleware, requestMiddleware] : [requestMiddleware]; +const createReduxStore = ( + reducers: Reducer, + initialState: DeepPartial, + routerMiddleware?: Middleware, + httpClient?: (request: () => Promise) => void +): Store => { + const middleware = routerMiddleware + ? [routerMiddleware, requestMiddleware(httpClient)] + : [requestMiddleware(httpClient)]; return createStore( getStoreReducer(reducers), initialState, // initial state, diff --git a/modules/module/client-react/ClientModule.ts b/modules/module/client-react/ClientModule.ts index a624c0a..236a297 100644 --- a/modules/module/client-react/ClientModule.ts +++ b/modules/module/client-react/ClientModule.ts @@ -15,6 +15,8 @@ export interface ClientModuleShape extends BaseModuleShape { stylesInsert?: string[]; // URL list to 3rd party js scripts scriptsInsert?: string[]; + // Http client is provided by modules + httpClient?: (request: () => Promise) => void; } interface ClientModule extends ClientModuleShape {} @@ -82,6 +84,13 @@ class ClientModule extends BaseModule { get scriptsInserts() { return this.scriptsInsert || []; } + + /** + * @returns Http client is provided by modules + */ + get httpClients() { + return this.httpClient; + } } export default ClientModule; diff --git a/modules/user/client-react/actions/register.ts b/modules/user/client-react/actions/register.ts index 365793f..b1edb34 100644 --- a/modules/user/client-react/actions/register.ts +++ b/modules/user/client-react/actions/register.ts @@ -5,6 +5,9 @@ import { ActionType } from '../reducers'; export default function REGISTER(value: RegisterSubmitProps) { return { types: [null, ActionType.REGISTER, null], - callAPI: () => axios.post(`${__API_URL__}/register`, { ...value }) + callAPI: (client: (request: () => Promise) => void) => + client + ? client(() => axios.post(`${__API_URL__}/register`, { ...value })) + : axios.post(`${__API_URL__}/register`, { ...value }) }; } From e0121053a14b246471e2f80ee767870fe41b19a8 Mon Sep 17 00:00:00 2001 From: Ivan Isakov Date: Wed, 8 May 2019 14:45:09 +0300 Subject: [PATCH 031/104] Add login and logout components --- modules/core/common/createReduxStore.ts | 6 +- .../{HeaderTitle.jsx => HeaderTitle.tsx} | 19 ++- modules/user/client-react/actions/index.ts | 3 +- modules/user/client-react/actions/login.ts | 13 ++ .../components/LoginForm.native.tsx | 157 ++++++++++++++++++ .../client-react/components/LoginForm.tsx | 115 +++++++++++++ .../components/LoginView.native.tsx | 110 ++++++++++++ .../client-react/components/LoginView.tsx | 72 ++++++++ .../user/client-react/containers/AuthBase.tsx | 35 +++- .../client-react/containers/Login.native.tsx | 39 +++++ .../user/client-react/containers/Login.tsx | 52 ++++++ .../client-react/containers/Logout.native.tsx | 32 ++++ .../user/client-react/containers/Register.tsx | 1 - modules/user/client-react/index.native.tsx | 31 +++- modules/user/client-react/index.tsx | 40 ++++- 15 files changed, 700 insertions(+), 25 deletions(-) rename modules/look/client-react-native/{HeaderTitle.jsx => HeaderTitle.tsx} (66%) create mode 100644 modules/user/client-react/actions/login.ts create mode 100644 modules/user/client-react/components/LoginForm.native.tsx create mode 100644 modules/user/client-react/components/LoginForm.tsx create mode 100644 modules/user/client-react/components/LoginView.native.tsx create mode 100644 modules/user/client-react/components/LoginView.tsx create mode 100644 modules/user/client-react/containers/Login.native.tsx create mode 100644 modules/user/client-react/containers/Login.tsx create mode 100644 modules/user/client-react/containers/Logout.native.tsx diff --git a/modules/core/common/createReduxStore.ts b/modules/core/common/createReduxStore.ts index bd4b566..dbf812b 100644 --- a/modules/core/common/createReduxStore.ts +++ b/modules/core/common/createReduxStore.ts @@ -23,13 +23,13 @@ const requestMiddleware: ( const handleCallApi = async () => { try { - const { data } = await callAPI(httpClient); + const result = await callAPI(httpClient); next({ type: SUCCESS, - payload: data, + payload: result, ...rest }); - return data; + return result; } catch (e) { const { response: { data } diff --git a/modules/look/client-react-native/HeaderTitle.jsx b/modules/look/client-react-native/HeaderTitle.tsx similarity index 66% rename from modules/look/client-react-native/HeaderTitle.jsx rename to modules/look/client-react-native/HeaderTitle.tsx index b7c5201..86f4fe4 100644 --- a/modules/look/client-react-native/HeaderTitle.jsx +++ b/modules/look/client-react-native/HeaderTitle.tsx @@ -1,20 +1,21 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { Text, StyleSheet, Platform } from 'react-native'; +import { TranslateFunction } from '@restapp/i18n-client-react'; -const HeaderTitle = ({ t, i18nKey, style, children, ...props }) => ( +interface HeaderTitleProps { + t?: TranslateFunction; + style?: any; + i18nKey?: string; + children?: React.ReactNode | any; + onPress?: () => any; +} + +const HeaderTitle = ({ t, i18nKey, style, children, ...props }: HeaderTitleProps) => ( {t ? t(i18nKey || 'navLink') : children} ); -HeaderTitle.propTypes = { - t: PropTypes.func, - children: PropTypes.node, - i18nKey: PropTypes.string, - style: PropTypes.any -}; - const styles = StyleSheet.create({ menuTitle: { padding: 16, diff --git a/modules/user/client-react/actions/index.ts b/modules/user/client-react/actions/index.ts index 98c39af..2ada4cf 100644 --- a/modules/user/client-react/actions/index.ts +++ b/modules/user/client-react/actions/index.ts @@ -1,3 +1,4 @@ import REGISTER from './register'; +import LOGIN from './login'; -export { REGISTER }; +export { REGISTER, LOGIN }; diff --git a/modules/user/client-react/actions/login.ts b/modules/user/client-react/actions/login.ts new file mode 100644 index 0000000..5dcbfb2 --- /dev/null +++ b/modules/user/client-react/actions/login.ts @@ -0,0 +1,13 @@ +import axios from 'axios'; +import { LoginSubmitProps } from '..'; +import { ActionType } from '../reducers'; + +export default function LOGIN(value: LoginSubmitProps) { + return { + types: [null, ActionType.SET_CURRENT_USER, ActionType.CLEAR_CURRENT_USER], + callAPI: (client: (request: () => Promise) => any) => + client + ? client(() => axios.post(`${__API_URL__}/login`, { ...value })) + : axios.post(`${__API_URL__}/login`, { ...value }) + }; +} diff --git a/modules/user/client-react/components/LoginForm.native.tsx b/modules/user/client-react/components/LoginForm.native.tsx new file mode 100644 index 0000000..4675a20 --- /dev/null +++ b/modules/user/client-react/components/LoginForm.native.tsx @@ -0,0 +1,157 @@ +import * as React from 'react'; +import { StyleSheet, View, Text, TouchableOpacity, ViewStyle } from 'react-native'; +import KeyboardSpacer from 'react-native-keyboard-spacer'; +import { withFormik } from 'formik'; +import { FieldAdapter as Field } from '@restapp/forms-client-react'; +import { translate, TranslateFunction } from '@restapp/i18n-client-react'; +import { RenderField, Button, primary, FormView } from '@restapp/look-client-react-native'; +import { placeholderColor, submit } from '@restapp/look-client-react-native/styles'; +import { required, minLength, validate } from '@restapp/validation-common-react'; +import { LinkedInButton, GoogleButton, GitHubButton, FacebookButton } from '@restapp/authentication-client-react'; + +import { FormProps, LoginSubmitProps, NavigationOptionsProps } from '../index.native'; +import settings from '../../../../settings'; + +interface SocialButtons { + buttonsLength: number; + t: TranslateFunction; +} + +interface LoginForm extends FormProps { + valid: string; +} + +const loginFormSchema = { + usernameOrEmail: [required, minLength(3)], + password: [required, minLength(settings.auth.password.minLength)] +}; +const { github, facebook, linkedin, google } = settings.auth.social; + +const renderSocialButtons = ({ buttonsLength, t }: SocialButtons) => { + const type: string = buttonsLength > 2 ? 'icon' : 'button'; + const containerStyle: ViewStyle = + buttonsLength > 2 ? { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' } : {}; + + return buttonsLength > 0 ? ( + + {facebook.enabled && } + {google.enabled && } + {github.enabled && } + {linkedin.enabled && } + + ) : null; +}; + +const LoginForm =

({ handleSubmit, valid, values, navigation, t }: P) => { + const buttonsLength = [facebook.enabled, linkedin.enabled, google.enabled, github.enabled].filter(button => button) + .length; + return ( + + + + + + + + + + + + {renderSocialButtons({ buttonsLength, t })} + + navigation.navigate('ForgotPassword')}> + {t('login.btn.forgotPass')} + + + + + + + {t('login.notRegText')} + navigation.navigate('Register')}> + {t('login.btn.sign')} + + + + + ); +}; + +const styles = StyleSheet.create({ + formContainer: { + flex: 1 + }, + formView: { + flex: 1, + alignSelf: 'stretch' + }, + form: { + justifyContent: 'center', + paddingHorizontal: 20, + flex: 9 + }, + submit, + buttonsGroup: { + flex: 1, + paddingTop: 10 + }, + buttonWrapper: { + flex: 1, + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'flex-end' + }, + text: { + fontSize: 14, + color: '#bcb8b8' + }, + signUpText: { + fontSize: 16, + paddingLeft: 3, + color: '#8e908c', + fontWeight: '600', + textDecorationLine: 'underline', + textAlign: 'center' + } +}); + +const LoginFormWithFormik = withFormik({ + enableReinitialize: true, + mapPropsToValues: () => ({ usernameOrEmail: '', password: '' }), + + handleSubmit(values, { setErrors, props: { onSubmit } }) { + onSubmit(values).catch((e: any) => { + if (e) { + setErrors(e.errors); + } else { + throw e; + } + }); + }, + validate: values => validate(values, loginFormSchema), + displayName: 'LoginForm' // helps with React DevTools +}); + +export default translate('user')(LoginFormWithFormik(LoginForm)); diff --git a/modules/user/client-react/components/LoginForm.tsx b/modules/user/client-react/components/LoginForm.tsx new file mode 100644 index 0000000..5084a55 --- /dev/null +++ b/modules/user/client-react/components/LoginForm.tsx @@ -0,0 +1,115 @@ +import * as React from 'react'; +import { withFormik } from 'formik'; +import { NavLink, Link } from 'react-router-dom'; +import { FieldAdapter as Field } from '@restapp/forms-client-react'; +import { translate, TranslateFunction } from '@restapp/i18n-client-react'; +import { required, minLength, validate } from '@restapp/validation-common-react'; +import { Form, RenderField, Alert, Button } from '@restapp/look-client-react'; +import { LinkedInButton, GoogleButton, GitHubButton, FacebookButton } from '@restapp/authentication-client-react'; + +import settings from '../../../../settings'; +import { FormProps, LoginSubmitProps } from '..'; + +interface SocialButtons { + buttonsLength: number; + t: TranslateFunction; +} + +const loginFormSchema = { + usernameOrEmail: [required, minLength(3)], + password: [required, minLength(8)] +}; +const { github, facebook, linkedin, google } = settings.auth.social; + +const renderSocialButtons = ({ buttonsLength, t }: SocialButtons) => { + const type: string = buttonsLength > 2 ? 'icon' : 'button'; + const containerStyle = + buttonsLength > 2 ? { display: 'flex', justifyContent: 'space-between', alignItems: 'center', minWidth: 200 } : {}; + + return ( +

+ {facebook.enabled && ( +
+ +
+ )} + {google.enabled && ( +
+ +
+ )} + {github.enabled && ( +
+ +
+ )} + {linkedin.enabled && ( +
+ +
+ )} +
+ ); +}; + +const LoginForm = ({ handleSubmit, submitting, errors, values, t }: FormProps) => { + const buttonsLength: number = [facebook.enabled, linkedin.enabled, google.enabled, github.enabled].filter( + button => button + ).length; + return ( +
+ + +
{errors && errors.message && {errors.message}}
+
+
+ +
+ {renderSocialButtons({ buttonsLength, t })} +
+
+ {t('login.btn.forgotPass')} +
+
+
+ {t('login.btn.notReg')} + + {t('login.btn.sign')} + +
+ + ); +}; + +const LoginFormWithFormik = withFormik, LoginSubmitProps>({ + enableReinitialize: true, + mapPropsToValues: () => ({ usernameOrEmail: '', password: '' }), + + handleSubmit(values, { setErrors, props: { onSubmit } }) { + onSubmit(values).catch((e: any) => { + if (e) { + setErrors(e.errors); + } else { + throw e; + } + }); + }, + validate: values => validate(values, loginFormSchema), + displayName: 'LoginForm' // helps with React DevTools +}); + +export default translate('user')(LoginFormWithFormik(LoginForm)); diff --git a/modules/user/client-react/components/LoginView.native.tsx b/modules/user/client-react/components/LoginView.native.tsx new file mode 100644 index 0000000..980e821 --- /dev/null +++ b/modules/user/client-react/components/LoginView.native.tsx @@ -0,0 +1,110 @@ +import * as React from 'react'; +import { StyleSheet, View, Text, Linking, Platform } from 'react-native'; +import { WebBrowser } from 'expo'; +import { translate } from '@restapp/i18n-client-react'; +import { placeholderColor } from '@restapp/look-client-react-native/styles'; +import { setItem } from '@restapp/core-common/clientStorage'; +import authentication from '@restapp/authentication-client-react'; + +import LoginForm from './LoginForm.native'; +import { LoginProps } from '../containers/Login.native'; + +import { LoginSubmitProps } from '../index.native'; + +interface LoginViewProps extends LoginProps { + onSubmit: (values: LoginSubmitProps) => void; +} + +interface LinkingEvent { + url: string; +} +class LoginView extends React.PureComponent { + public componentDidMount() { + Linking.addEventListener('url', this.handleOpenURL); + } + + public componentWillUnmount() { + Linking.removeEventListener('url', this.handleOpenURL); + } + + public handleOpenURL = async ({ url }: LinkingEvent) => { + const dataRegExp = /data=([^#]+)/; + if (!url.match(dataRegExp)) { + return; + } + + // Extract stringified user string out of the URL + const [, data] = url.match(dataRegExp); + const decodedData = JSON.parse(decodeURI(data)); + + if (decodedData.tokens) { + await setItem('accessToken', decodedData.tokens.accessToken); + await setItem('refreshToken', decodedData.tokens.refreshToken); + + await authentication.doLogin(); + } + + if (Platform.OS === 'ios') { + WebBrowser.dismissBrowser(); + } + }; + + public renderAvailableLogins = () => ( + + {this.props.t('login.cardTitle')}: + admin@example.com: admin123 + user@example.com: user1234 + + ); + + public render() { + const { navigation, onSubmit } = this.props; + return ( + + {this.renderAvailableLogins()} + + + + + ); + } +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'stretch', + padding: 10 + }, + examplesArea: { + borderWidth: 0.5, + borderRadius: 5, + borderColor: placeholderColor, + justifyContent: 'space-between', + alignItems: 'center', + backgroundColor: '#e3e3e3', + padding: 10 + }, + examplesContainer: { + flex: 1, + alignItems: 'center', + justifyContent: 'center' + }, + title: { + fontSize: 18, + fontWeight: '600', + textAlign: 'center', + color: placeholderColor + }, + exampleText: { + fontSize: 14, + fontWeight: '400', + color: placeholderColor + }, + loginContainer: { + flex: 3 + } +}); + +export default translate('user')(LoginView); diff --git a/modules/user/client-react/components/LoginView.tsx b/modules/user/client-react/components/LoginView.tsx new file mode 100644 index 0000000..43798b0 --- /dev/null +++ b/modules/user/client-react/components/LoginView.tsx @@ -0,0 +1,72 @@ +import * as React from 'react'; +import Helmet from 'react-helmet'; + +import { LayoutCenter, PageLayout, Card, CardGroup, CardTitle, CardText, Button } from '@restapp/look-client-react'; +import { translate, TranslateFunction } from '@restapp/i18n-client-react'; + +import LoginForm from './LoginForm'; +import { LoginSubmitProps } from '..'; +import { LoginProps } from '../containers/Login'; + +import settings from '../../../../settings'; + +interface LoginViewProps extends LoginProps { + onSubmit: (values: LoginSubmitProps) => void; + t: TranslateFunction; + isRegistered?: boolean; + hideModal: () => void; +} + +const LoginView = ({ onSubmit, t, isRegistered, hideModal }: LoginViewProps) => { + const renderMetaData = () => ( + + ); + + const renderConfirmationModal = () => ( + + + {t('reg.successRegTitle')} + {t('reg.successRegBody')} + + + + + + ); + + return ( + + {renderMetaData()} + + {isRegistered ? ( + renderConfirmationModal() + ) : ( + <> +

{t('login.form.title')}

+ +
+ + + {t('login.cardTitle')}: + admin@example.com:admin123 + user@example.com:user1234 + + + + )} +
+
+ ); +}; + +export default translate('user')(LoginView); diff --git a/modules/user/client-react/containers/AuthBase.tsx b/modules/user/client-react/containers/AuthBase.tsx index cbaefcf..c39d21c 100644 --- a/modules/user/client-react/containers/AuthBase.tsx +++ b/modules/user/client-react/containers/AuthBase.tsx @@ -2,8 +2,9 @@ import * as React from 'react'; import { RouteProps } from 'react-router'; import { History } from 'history'; import { connect } from 'react-redux'; +import authentication from '@restapp/authentication-client-react'; import { User, UserRole } from '..'; - +import { ActionType } from '../reducers'; export interface WithUserProps extends RouteProps { currentUser?: User; currentUserLoading?: boolean; @@ -48,10 +49,40 @@ const withLoadedUser = (Component: React.ComponentType) => { return withUser(WithLoadedUser); }; +const IfLoggedInComponent: React.FunctionComponent = ({ + currentUser, + role, + children, + elseComponent +}) => (hasRole(role, currentUser) ? React.cloneElement(children, {}) : elseComponent || null); + +const IfLoggedIn: React.ComponentType = withLoadedUser(IfLoggedInComponent); + const IfNotLoggedInComponent: React.FunctionComponent = ({ currentUser, children }) => { return !currentUser ? React.cloneElement(children, {}) : null; }; const IfNotLoggedIn: React.ComponentType = withLoadedUser(IfNotLoggedInComponent); -export { withUser, hasRole, withLoadedUser, IfNotLoggedIn }; +const withLogout: any = (Component: React.ComponentType) => { + const WithLogout = ({ clearUser, ...props }: WithLogoutProps) => { + const newProps = { + ...props, + logout: () => authentication.doLogout(clearUser) + }; + return ; + }; + return connect( + null, + dispatch => { + return { + clearUser: () => + dispatch({ + type: ActionType.CLEAR_CURRENT_USER + }) + }; + } + )(WithLogout); +}; + +export { withUser, hasRole, withLoadedUser, IfLoggedIn, IfNotLoggedIn, withLogout }; diff --git a/modules/user/client-react/containers/Login.native.tsx b/modules/user/client-react/containers/Login.native.tsx new file mode 100644 index 0000000..40bc60c --- /dev/null +++ b/modules/user/client-react/containers/Login.native.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { FormError } from '@restapp/forms-client-react'; +import { translate } from '@restapp/i18n-client-react'; +import authentication from '@restapp/authentication-client-react'; +import { connect } from 'react-redux'; +import { compose } from 'redux'; +import LoginView from '../components/LoginView.native'; +import { CommonProps, LoginSubmitProps } from '../index.native'; +import { LOGIN } from '../actions'; + +export interface LoginProps extends CommonProps { + login: (values: LoginSubmitProps) => Promise | any; +} + +const Login = (props: LoginProps) => { + const { t, login } = props; + + const onSubmit = async (values: LoginSubmitProps) => { + try { + await login(values); + } catch (e) { + throw new FormError(t('login.errorMsg'), e); + } + + await authentication.doLogin(); + }; + + return ; +}; + +const withConnect = connect( + null, + { login: LOGIN } +); + +export default compose( + translate('user'), + withConnect(Login) +); diff --git a/modules/user/client-react/containers/Login.tsx b/modules/user/client-react/containers/Login.tsx new file mode 100644 index 0000000..8391df1 --- /dev/null +++ b/modules/user/client-react/containers/Login.tsx @@ -0,0 +1,52 @@ +import * as React from 'react'; +import { connect } from 'react-redux'; +import { translate } from '@restapp/i18n-client-react'; +import authentication from '@restapp/authentication-client-react'; +import { FormError } from '@restapp/forms-client-react'; + +import LoginView from '../components/LoginView'; +import { CommonProps, LoginSubmitProps } from '..'; + +import { LOGIN } from '../actions'; + +export interface LoginProps extends CommonProps { + login: (values: LoginSubmitProps) => any; +} + +const Login: React.FunctionComponent = props => { + const { t, history, login } = props; + const { + location: { search } + } = history; + + const [isRegistered, setIsRegistered] = React.useState(false); + const [isReady, setIsReady] = React.useState(false); + + React.useEffect(() => { + if (search.includes('email-verified')) { + setIsRegistered(true); + } + setIsReady(true); + }, []); + + const hideModal = () => { + setIsRegistered(false); + history.push({ search: '' }); + }; + + const onSubmit = async (values: LoginSubmitProps) => { + const data = await login(values); + + if (data.errors) { + throw new FormError(t('reg.errorMsg'), data); + } + await authentication.doLogin(); + }; + + return isReady && ; +}; + +export default connect( + null, + { login: LOGIN } +)(translate('user')(Login)); diff --git a/modules/user/client-react/containers/Logout.native.tsx b/modules/user/client-react/containers/Logout.native.tsx new file mode 100644 index 0000000..bdc21d3 --- /dev/null +++ b/modules/user/client-react/containers/Logout.native.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { View } from 'react-native'; +import { HeaderTitle } from '@restapp/look-client-react-native'; +import { translate } from '@restapp/i18n-client-react'; + +import { withLogout } from './Auth'; + +import { CommonProps } from '../index.native'; + +interface LogoutViewProps extends CommonProps { + logout: () => void; +} + +const LogoutView = ({ logout, t }: LogoutViewProps) => { + return ( + + { + await logout(); + }} + > + {t('mobile.logout')} + + + ); +}; + +export default translate('user')(withLogout(LogoutView)); diff --git a/modules/user/client-react/containers/Register.tsx b/modules/user/client-react/containers/Register.tsx index 5fbfb85..4af4b22 100644 --- a/modules/user/client-react/containers/Register.tsx +++ b/modules/user/client-react/containers/Register.tsx @@ -11,7 +11,6 @@ import settings from '../../../../settings'; interface RegisterProps extends CommonProps { register: (values: RegisterSubmitProps) => any; - data: any; } const Register: React.FunctionComponent = props => { diff --git a/modules/user/client-react/index.native.tsx b/modules/user/client-react/index.native.tsx index d9eb2e0..b02615b 100644 --- a/modules/user/client-react/index.native.tsx +++ b/modules/user/client-react/index.native.tsx @@ -7,12 +7,14 @@ import { NavigationParams } from 'react-navigation'; import { translate, TranslateFunction } from '@restapp/i18n-client-react'; -import { HeaderTitle } from '@restapp/look-client-react-native'; +import { HeaderTitle, IconButton } from '@restapp/look-client-react-native'; import ClientModule from '@restapp/module-client-react-native'; import { FormikErrors } from 'formik'; import resources from './locales'; import DataRootComponent from './containers/DataRootComponent.native'; import UserScreenNavigator from './containers/UserScreenNavigator.native'; +import Login from './containers/Login.native'; +import Logout from './containers/Logout.native'; import Register from './containers/Register.native'; import reducers from './reducers'; @@ -97,6 +99,19 @@ export interface Filter { isActive: boolean; } +class LoginScreen extends React.Component { + public static navigationOptions = ({ navigation }: NavigationOptionsProps) => ({ + headerTitle: , + headerLeft: ( + navigation.openDrawer()} /> + ), + headerForceInset: {} + }); + + public render() { + return ; + } +} class RegisterScreen extends React.Component { public static navigationOptions = () => ({ headerTitle: , @@ -109,6 +124,7 @@ class RegisterScreen extends React.Component { const AuthScreen = createStackNavigator( { + Login: { screen: LoginScreen }, Register: { screen: RegisterScreen } }, { @@ -143,7 +159,18 @@ export default new ClientModule({ showOnLogin: false }, navigationOptions: { - drawerLabel: + drawerLabel: + } + }, + Logout: { + screen: (): null => null, + userInfo: { + showOnLogin: true + }, + navigationOptions: ({ navigation }: NavigationOptionsProps) => { + return { + drawerLabel: + }; } } } diff --git a/modules/user/client-react/index.tsx b/modules/user/client-react/index.tsx index 0f2f7ac..44b5d86 100644 --- a/modules/user/client-react/index.tsx +++ b/modules/user/client-react/index.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import * as H from 'history'; import { CookiesProvider } from 'react-cookie'; -import { NavLink } from 'react-router-dom'; +import { NavLink, withRouter } from 'react-router-dom'; import { translate, TranslateFunction } from '@restapp/i18n-client-react'; import { MenuItem } from '@restapp/look-client-react'; import ClientModule from '@restapp/module-client-react'; @@ -10,10 +10,10 @@ import { FormikErrors } from 'formik'; import resources from './locales'; import DataRootComponent from './containers/DataRootComponent'; import Register from './containers/Register'; - +import Login from './containers/Login'; import reducers from './reducers'; -import { AuthRoute, IfNotLoggedIn } from './containers/Auth'; +import { AuthRoute, IfLoggedIn, IfNotLoggedIn, withLogout, WithLogoutProps } from './containers/Auth'; export enum UserRole { admin = 'admin', @@ -88,18 +88,44 @@ export interface Filter { isActive: boolean; } +const LogoutLink = withRouter( + withLogout(({ logout, history }: WithLogoutProps) => ( + { + e.preventDefault(); + (async () => { + await logout(); + history.push('/'); + })(); + }} + className="nav-link" + > + Logout + + )) +); + export * from './containers/Auth'; const NavLinkLoginWithI18n = translate('user')(({ t }: any) => ( - - {t('navLink.signUp')} + + {t('navLink.signIn')} )); export default new ClientModule({ - route: [], + route: [ + , + + ], navItemRight: [ - + + + + + , + From 8abe1f680ea28c1b8885b0802c5fb63e98b148fa Mon Sep 17 00:00:00 2001 From: Alexandr Belokur Date: Wed, 8 May 2019 15:40:13 +0300 Subject: [PATCH 032/104] Add authentication to modules --- .gitignore | 1 + modules/user/server-ts/index.ts | 45 ++++++++++++++++++- .../user/server-ts/password/controllers.ts | 4 +- packages/server/src/modules.ts | 3 +- 4 files changed, 48 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 49ac726..579ab5a 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ jsconfig.json public *.iml .npmrc +packages/server/sessions diff --git a/modules/user/server-ts/index.ts b/modules/user/server-ts/index.ts index dca713e..817cdaa 100644 --- a/modules/user/server-ts/index.ts +++ b/modules/user/server-ts/index.ts @@ -1,6 +1,11 @@ +import bcrypt from 'bcryptjs'; +import i18n from 'i18next'; + import ServerModule from '@restapp/module-server-ts'; -import password from './password'; +import password from './password'; +import UserDAO, { UserShape } from './sql'; +import settings from '../../../settings'; import resources from './locales'; export interface ValidationErrors { @@ -9,4 +14,40 @@ export interface ValidationErrors { password?: string; } -export default new ServerModule(password, { localization: [{ ns: 'user', resources }] }); +const getIdentity = (id: number) => { + return UserDAO.getUser(id); +}; + +const getHash = async (id: number) => ((await UserDAO.getUserWithPassword(id)) as UserShape).passwordHash || ''; + +const validateLogin = async (usernameOrEmail: string, pswd: string) => { + const identity = (await UserDAO.getUserByUsernameOrEmail(usernameOrEmail)) as UserShape; + + if (!identity) { + return { message: i18n.t('user:auth.password.validPasswordEmail') }; + } + + if (settings.auth.password.requireEmailConfirmation && !identity.isActive) { + return { message: i18n.t('user:auth.password.emailConfirmation') }; + } + + const valid = await bcrypt.compare(pswd, identity.passwordHash); + if (!valid) { + return { message: i18n.t('user:auth.password.validPassword') }; + } + + return { identity }; +}; + +const appContext = { + user: { + getIdentity, + getHash, + validateLogin + } +}; + +export default new ServerModule(password, { + appContext, + localization: [{ ns: 'user', resources }] +}); diff --git a/modules/user/server-ts/password/controllers.ts b/modules/user/server-ts/password/controllers.ts index a51b0f0..22390e5 100644 --- a/modules/user/server-ts/password/controllers.ts +++ b/modules/user/server-ts/password/controllers.ts @@ -17,7 +17,7 @@ const { app } = settings; -export const login = (req: any, res: any) => { +export const login = (req: any, res: any, next: any) => { passport.authenticate('local', { session: session.enabled }, (err, user, info) => { if (err || !user) { return res.status(400).json({ @@ -34,7 +34,7 @@ export const login = (req: any, res: any) => { return res.json({ user, tokens }); }); - })(req, res); + })(req, res, next); }; export const register = async ({ body, t }: any, res: any) => { diff --git a/packages/server/src/modules.ts b/packages/server/src/modules.ts index fe5ba12..28c4d6d 100644 --- a/packages/server/src/modules.ts +++ b/packages/server/src/modules.ts @@ -5,10 +5,11 @@ import validation from '@restapp/validation-common-react'; import cookies from '@restapp/cookies-server-ts'; import mailer from '@restapp/mailer-server-ts'; import user from '@restapp/user-server-ts'; +import authentication from '@restapp/authentication-server-ts'; import '@restapp/debug-server-ts'; import ServerModule from '@restapp/module-server-ts'; -const modules: ServerModule = new ServerModule(welcome, cookies, i18n, validation, mailer, core, user); +const modules: ServerModule = new ServerModule(authentication, welcome, cookies, i18n, validation, mailer, core, user); export default modules; From 257522370847a37f4bc7862aa95a0ceb8109e1c5 Mon Sep 17 00:00:00 2001 From: Alexandr Belokur Date: Wed, 8 May 2019 17:17:49 +0300 Subject: [PATCH 033/104] Add controller for current user --- modules/user/server-ts/controllers.ts | 9 +++++++++ modules/user/server-ts/index.ts | 13 +++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 modules/user/server-ts/controllers.ts diff --git a/modules/user/server-ts/controllers.ts b/modules/user/server-ts/controllers.ts new file mode 100644 index 0000000..149be28 --- /dev/null +++ b/modules/user/server-ts/controllers.ts @@ -0,0 +1,9 @@ +import userDAO from './sql'; + +export const currentUser = async ({ user: identity }: any, res: any) => { + if (identity.id) { + res.json(await userDAO.getUser(identity.id)); + } else { + res.send(null); + } +}; diff --git a/modules/user/server-ts/index.ts b/modules/user/server-ts/index.ts index 817cdaa..2421d7f 100644 --- a/modules/user/server-ts/index.ts +++ b/modules/user/server-ts/index.ts @@ -1,8 +1,9 @@ import bcrypt from 'bcryptjs'; import i18n from 'i18next'; -import ServerModule from '@restapp/module-server-ts'; +import ServerModule, { RestMethod } from '@restapp/module-server-ts'; +import { currentUser } from './controllers'; import password from './password'; import UserDAO, { UserShape } from './sql'; import settings from '../../../settings'; @@ -49,5 +50,13 @@ const appContext = { export default new ServerModule(password, { appContext, - localization: [{ ns: 'user', resources }] + localization: [{ ns: 'user', resources }], + apiRouteParams: [ + { + method: RestMethod.GET, + route: 'currentUser', + isAuthRoute: true, + controller: currentUser + } + ] }); From 45fd75f4250b37418b5d1f24c791b4bc4e2f70fd Mon Sep 17 00:00:00 2001 From: Ivan Isakov Date: Wed, 8 May 2019 17:21:05 +0300 Subject: [PATCH 034/104] Fix handle response --- .../client-react/access/index.tsx | 3 +- .../client-react/access/jwt/index.ts | 6 ++-- modules/core/common/createReduxStore.ts | 4 +-- .../user/client-react/actions/currentUser.ts | 13 ++++++++ modules/user/client-react/actions/index.ts | 3 +- .../user/client-react/containers/AuthBase.tsx | 17 ++++++---- .../user/client-react/containers/Login.tsx | 2 +- modules/user/client-react/index.tsx | 32 +++++++++---------- modules/user/client-react/reducers/index.ts | 2 +- 9 files changed, 48 insertions(+), 34 deletions(-) create mode 100644 modules/user/client-react/actions/currentUser.ts diff --git a/modules/authentication/client-react/access/index.tsx b/modules/authentication/client-react/access/index.tsx index 5159469..e91c031 100644 --- a/modules/authentication/client-react/access/index.tsx +++ b/modules/authentication/client-react/access/index.tsx @@ -15,8 +15,7 @@ const rerenderApp = () => { ref.current.reloadPage(); }; -const login = async (clearStore: () => void) => { - await clearStore(); +const login = async (_clearStore?: () => void) => { rerenderApp(); }; diff --git a/modules/authentication/client-react/access/jwt/index.ts b/modules/authentication/client-react/access/jwt/index.ts index dc9edfc..11694eb 100644 --- a/modules/authentication/client-react/access/jwt/index.ts +++ b/modules/authentication/client-react/access/jwt/index.ts @@ -65,12 +65,10 @@ axios.interceptors.request.use(async config => { axios.interceptors.response.use(async (res: any) => { if (res.config.url.includes('login')) { - if (!!res.data && res.data.login.tokens) { + if (!!res.data && res.data.tokens) { const { data: { - login: { - tokens: { accessToken, refreshToken } - } + tokens: { accessToken, refreshToken } } } = res; await saveTokens({ accessToken, refreshToken }); diff --git a/modules/core/common/createReduxStore.ts b/modules/core/common/createReduxStore.ts index dbf812b..34c1e19 100644 --- a/modules/core/common/createReduxStore.ts +++ b/modules/core/common/createReduxStore.ts @@ -26,10 +26,10 @@ const requestMiddleware: ( const result = await callAPI(httpClient); next({ type: SUCCESS, - payload: result, + payload: result.data, ...rest }); - return result; + return result.data; } catch (e) { const { response: { data } diff --git a/modules/user/client-react/actions/currentUser.ts b/modules/user/client-react/actions/currentUser.ts new file mode 100644 index 0000000..899b17d --- /dev/null +++ b/modules/user/client-react/actions/currentUser.ts @@ -0,0 +1,13 @@ +import axios from 'axios'; +import { LoginSubmitProps } from '..'; +import { ActionType } from '../reducers'; + +export default function CURRENT_USER(value: LoginSubmitProps) { + return { + types: [null, ActionType.SET_CURRENT_USER, ActionType.CLEAR_CURRENT_USER], + callAPI: (client: (request: () => Promise) => any) => + client + ? client(() => axios.post(`${__API_URL__}/currentUser`, { ...value })) + : axios.post(`${__API_URL__}/currentUser`, { ...value }) + }; +} diff --git a/modules/user/client-react/actions/index.ts b/modules/user/client-react/actions/index.ts index 2ada4cf..178f258 100644 --- a/modules/user/client-react/actions/index.ts +++ b/modules/user/client-react/actions/index.ts @@ -1,4 +1,5 @@ import REGISTER from './register'; import LOGIN from './login'; +import CURRENT_USER from './currentUser'; -export { REGISTER, LOGIN }; +export { REGISTER, LOGIN, CURRENT_USER }; diff --git a/modules/user/client-react/containers/AuthBase.tsx b/modules/user/client-react/containers/AuthBase.tsx index c39d21c..a4ba1b6 100644 --- a/modules/user/client-react/containers/AuthBase.tsx +++ b/modules/user/client-react/containers/AuthBase.tsx @@ -5,6 +5,8 @@ import { connect } from 'react-redux'; import authentication from '@restapp/authentication-client-react'; import { User, UserRole } from '..'; import { ActionType } from '../reducers'; +import { CURRENT_USER } from '../actions'; + export interface WithUserProps extends RouteProps { currentUser?: User; currentUserLoading?: boolean; @@ -28,13 +30,16 @@ export interface WithLogoutProps extends WithUserProps { } const withUser = (Component: React.ComponentType) => { - const WithUser = ({ currentUser, ...rest }: WithUserProps) => { + const WithUser = ({ currentUser, getCurrentUser, ...rest }: WithUserProps) => { return ; }; - return connect(({ user: { loading, currentUser } }: any) => ({ - currentUserLoading: loading, - currentUser - }))(WithUser); + return connect( + ({ user: { loading, currentUser } }: any) => ({ + currentUserLoading: loading, + currentUser + }), + { getCurrentUser: CURRENT_USER } + )(WithUser); }; const hasRole = (role: UserRole | UserRole[], currentUser: User) => { @@ -64,7 +69,7 @@ const IfNotLoggedInComponent: React.FunctionComponent = ({ const IfNotLoggedIn: React.ComponentType = withLoadedUser(IfNotLoggedInComponent); -const withLogout: any = (Component: React.ComponentType) => { +const withLogout = (Component: React.ComponentType) => { const WithLogout = ({ clearUser, ...props }: WithLogoutProps) => { const newProps = { ...props, diff --git a/modules/user/client-react/containers/Login.tsx b/modules/user/client-react/containers/Login.tsx index 8391df1..85aa2bf 100644 --- a/modules/user/client-react/containers/Login.tsx +++ b/modules/user/client-react/containers/Login.tsx @@ -37,7 +37,7 @@ const Login: React.FunctionComponent = props => { const onSubmit = async (values: LoginSubmitProps) => { const data = await login(values); - if (data.errors) { + if (data && data.errors) { throw new FormError(t('reg.errorMsg'), data); } await authentication.doLogin(); diff --git a/modules/user/client-react/index.tsx b/modules/user/client-react/index.tsx index 44b5d86..bb9976d 100644 --- a/modules/user/client-react/index.tsx +++ b/modules/user/client-react/index.tsx @@ -88,23 +88,21 @@ export interface Filter { isActive: boolean; } -const LogoutLink = withRouter( - withLogout(({ logout, history }: WithLogoutProps) => ( - { - e.preventDefault(); - (async () => { - await logout(); - history.push('/'); - })(); - }} - className="nav-link" - > - Logout - - )) -); +const LogoutLink = withRouter(withLogout(({ logout, history }: WithLogoutProps) => ( + { + e.preventDefault(); + (async () => { + await logout(); + history.push('/'); + })(); + }} + className="nav-link" + > + Logout + +)) as any); export * from './containers/Auth'; diff --git a/modules/user/client-react/reducers/index.ts b/modules/user/client-react/reducers/index.ts index 87478dd..ae9007c 100644 --- a/modules/user/client-react/reducers/index.ts +++ b/modules/user/client-react/reducers/index.ts @@ -51,7 +51,7 @@ export default function(state = defaultState, action: UserModuleActionProps) { case ActionType.SET_CURRENT_USER: return { ...state, - currentUser: action.payload, + currentUser: action.payload.user || action.payload, loading: false }; From 231245b3f0ba5f2400b55b2783f281dcd9d04e34 Mon Sep 17 00:00:00 2001 From: Ivan Isakov Date: Thu, 9 May 2019 10:57:08 +0300 Subject: [PATCH 035/104] Add loading component and Fix login with logout --- .../client-react/access/jwt/index.ts | 11 ++---- .../client-react/access/session/index.ts | 3 +- modules/core/common/createReduxStore.ts | 13 +++---- .../user/client-react/actions/clearUser.ts | 7 ++++ .../user/client-react/actions/currentUser.ts | 7 ++-- modules/user/client-react/actions/index.ts | 3 +- modules/user/client-react/actions/register.ts | 2 +- .../components/Loading.native.tsx | 19 +++++++++++ .../user/client-react/components/Loading.tsx | 16 +++++++++ .../user/client-react/containers/AuthBase.tsx | 26 +++++--------- .../containers/DataRootComponent.tsx | 34 +++++++++++++++++-- modules/user/client-react/reducers/index.ts | 2 +- 12 files changed, 96 insertions(+), 47 deletions(-) create mode 100644 modules/user/client-react/actions/clearUser.ts create mode 100644 modules/user/client-react/components/Loading.native.tsx create mode 100644 modules/user/client-react/components/Loading.tsx diff --git a/modules/authentication/client-react/access/jwt/index.ts b/modules/authentication/client-react/access/jwt/index.ts index 11694eb..7f2c750 100644 --- a/modules/authentication/client-react/access/jwt/index.ts +++ b/modules/authentication/client-react/access/jwt/index.ts @@ -32,8 +32,8 @@ const client = async (request: () => Promise) => { const { data } = await axios.post(`${__API_URL__}/refreshToken`, { refreshToken: await getItem('refreshToken') }); - if (data && data.refreshTokens) { - const { accessToken, refreshToken } = data.refreshTokens; + if (data) { + const { accessToken, refreshToken } = data; await saveTokens({ accessToken, refreshToken }); } else { await removeTokens(); @@ -51,15 +51,10 @@ const client = async (request: () => Promise) => { axios.interceptors.request.use(async config => { const accessToken = await getItem(TokensEnum.accessToken); - if (config.url.includes('currentUser') && !(await getItem(TokensEnum.refreshToken))) { - throw new axios.Cancel('Operation canceled'); - } - const arrayExceptions = ['login', 'refreshTokens']; const checkInclude = arrayExceptions.some(exception => config.url.includes(exception)); config.headers = !checkInclude && accessToken ? { Authorization: `Bearer ${accessToken}` } : {}; - return config; }); @@ -75,8 +70,8 @@ axios.interceptors.response.use(async (res: any) => { } else { await removeTokens(); } - return res; } + return res; }); export default (settings.auth.jwt.enabled diff --git a/modules/authentication/client-react/access/session/index.ts b/modules/authentication/client-react/access/session/index.ts index 47a5955..3688ab1 100644 --- a/modules/authentication/client-react/access/session/index.ts +++ b/modules/authentication/client-react/access/session/index.ts @@ -3,9 +3,8 @@ import AccessModule from '../AccessModule'; import axios from 'axios'; const logout = async () => { - await axios.post(`${__API_URL__}/logout`); + axios.post(`${__API_URL__}/logout`); }; - export default (settings.auth.session.enabled ? new AccessModule({ logout: [logout] diff --git a/modules/core/common/createReduxStore.ts b/modules/core/common/createReduxStore.ts index 34c1e19..825d0a2 100644 --- a/modules/core/common/createReduxStore.ts +++ b/modules/core/common/createReduxStore.ts @@ -8,9 +8,7 @@ export const getStoreReducer = (reducers: any) => ...reducers }); -const requestMiddleware: ( - httpClient?: (request: () => Promise) => void -) => Middleware = httpClient => _state => next => action => { +const requestMiddleware: (httpClient?: any) => Middleware = httpClient => _state => next => action => { const { types, callAPI, ...rest } = action; if (!types) { @@ -24,16 +22,15 @@ const requestMiddleware: ( const handleCallApi = async () => { try { const result = await callAPI(httpClient); + const data = result && result.data; next({ type: SUCCESS, - payload: result.data, + payload: data, ...rest }); - return result.data; + return data; } catch (e) { - const { - response: { data } - } = e; + const data = e.response && e.response.data; next({ type: FAIL, payload: data, diff --git a/modules/user/client-react/actions/clearUser.ts b/modules/user/client-react/actions/clearUser.ts new file mode 100644 index 0000000..357e0d8 --- /dev/null +++ b/modules/user/client-react/actions/clearUser.ts @@ -0,0 +1,7 @@ +import { ActionType } from '../reducers'; + +export default function CLEAR_USER() { + return { + type: ActionType.CLEAR_CURRENT_USER + }; +} diff --git a/modules/user/client-react/actions/currentUser.ts b/modules/user/client-react/actions/currentUser.ts index 899b17d..87dea99 100644 --- a/modules/user/client-react/actions/currentUser.ts +++ b/modules/user/client-react/actions/currentUser.ts @@ -1,13 +1,10 @@ import axios from 'axios'; -import { LoginSubmitProps } from '..'; import { ActionType } from '../reducers'; -export default function CURRENT_USER(value: LoginSubmitProps) { +export default function CURRENT_USER() { return { types: [null, ActionType.SET_CURRENT_USER, ActionType.CLEAR_CURRENT_USER], callAPI: (client: (request: () => Promise) => any) => - client - ? client(() => axios.post(`${__API_URL__}/currentUser`, { ...value })) - : axios.post(`${__API_URL__}/currentUser`, { ...value }) + client ? client(() => axios.get(`${__API_URL__}/currentUser`)) : axios.get(`${__API_URL__}/currentUser`) }; } diff --git a/modules/user/client-react/actions/index.ts b/modules/user/client-react/actions/index.ts index 178f258..6e9ce09 100644 --- a/modules/user/client-react/actions/index.ts +++ b/modules/user/client-react/actions/index.ts @@ -1,5 +1,6 @@ import REGISTER from './register'; import LOGIN from './login'; import CURRENT_USER from './currentUser'; +import CLEAR_USER from './clearUser'; -export { REGISTER, LOGIN, CURRENT_USER }; +export { REGISTER, LOGIN, CURRENT_USER, CLEAR_USER }; diff --git a/modules/user/client-react/actions/register.ts b/modules/user/client-react/actions/register.ts index b1edb34..ed7bd8b 100644 --- a/modules/user/client-react/actions/register.ts +++ b/modules/user/client-react/actions/register.ts @@ -5,7 +5,7 @@ import { ActionType } from '../reducers'; export default function REGISTER(value: RegisterSubmitProps) { return { types: [null, ActionType.REGISTER, null], - callAPI: (client: (request: () => Promise) => void) => + callAPI: (client: (request: () => Promise) => any) => client ? client(() => axios.post(`${__API_URL__}/register`, { ...value })) : axios.post(`${__API_URL__}/register`, { ...value }) diff --git a/modules/user/client-react/components/Loading.native.tsx b/modules/user/client-react/components/Loading.native.tsx new file mode 100644 index 0000000..0ba6c84 --- /dev/null +++ b/modules/user/client-react/components/Loading.native.tsx @@ -0,0 +1,19 @@ +import React from 'react'; + +import { View, Text } from 'react-native'; +import { translate } from '@restapp/i18n-client-react'; +import { LayoutCenter } from '@restapp/look-client-react-native'; + +import { CommonProps } from '../index.native'; + +interface LoadingProps extends CommonProps {} + +const Loading = ({ t }: LoadingProps) => ( + + + {t('loading')} + + +); + +export default translate('user')(Loading); diff --git a/modules/user/client-react/components/Loading.tsx b/modules/user/client-react/components/Loading.tsx new file mode 100644 index 0000000..810acb1 --- /dev/null +++ b/modules/user/client-react/components/Loading.tsx @@ -0,0 +1,16 @@ +import React from 'react'; + +import { translate } from '@restapp/i18n-client-react'; +import { LayoutCenter } from '@restapp/look-client-react'; + +import { CommonProps } from '..'; + +interface LoadingProps extends CommonProps {} + +const Loading = ({ t }: LoadingProps) => ( + +
{t('loading')}
+
+); + +export default translate('user')(Loading); diff --git a/modules/user/client-react/containers/AuthBase.tsx b/modules/user/client-react/containers/AuthBase.tsx index a4ba1b6..7cf99d9 100644 --- a/modules/user/client-react/containers/AuthBase.tsx +++ b/modules/user/client-react/containers/AuthBase.tsx @@ -4,8 +4,7 @@ import { History } from 'history'; import { connect } from 'react-redux'; import authentication from '@restapp/authentication-client-react'; import { User, UserRole } from '..'; -import { ActionType } from '../reducers'; -import { CURRENT_USER } from '../actions'; +import CLEAR_USER from '../actions/clearUser'; export interface WithUserProps extends RouteProps { currentUser?: User; @@ -30,16 +29,13 @@ export interface WithLogoutProps extends WithUserProps { } const withUser = (Component: React.ComponentType) => { - const WithUser = ({ currentUser, getCurrentUser, ...rest }: WithUserProps) => { + const WithUser = ({ currentUser, ...rest }: WithUserProps) => { return ; }; - return connect( - ({ user: { loading, currentUser } }: any) => ({ - currentUserLoading: loading, - currentUser - }), - { getCurrentUser: CURRENT_USER } - )(WithUser); + return connect(({ user: { loading, currentUser } }: any) => ({ + currentUserLoading: loading, + currentUser + }))(WithUser); }; const hasRole = (role: UserRole | UserRole[], currentUser: User) => { @@ -77,16 +73,10 @@ const withLogout = (Component: React.ComponentType) => { }; return ; }; + return connect( null, - dispatch => { - return { - clearUser: () => - dispatch({ - type: ActionType.CLEAR_CURRENT_USER - }) - }; - } + { clearUser: CLEAR_USER } )(WithLogout); }; diff --git a/modules/user/client-react/containers/DataRootComponent.tsx b/modules/user/client-react/containers/DataRootComponent.tsx index 4dc0f40..62eac81 100644 --- a/modules/user/client-react/containers/DataRootComponent.tsx +++ b/modules/user/client-react/containers/DataRootComponent.tsx @@ -1,11 +1,39 @@ import * as React from 'react'; +import { connect } from 'react-redux'; +import { getItem } from '@restapp/core-common/clientStorage'; + +import Loading from '../components/Loading'; +import { UserModuleState } from '../reducers'; +import { CURRENT_USER } from '../actions'; +import { User } from '..'; interface DataRootComponent { + currentUser: User; + getCurrentUser: () => void; children?: Element | any; } -const DataRootComponent: React.FunctionComponent = props => { - return props.children; +const DataRootComponent: React.FunctionComponent = ({ currentUser, children, getCurrentUser }) => { + const [ready, setReady] = React.useState(false); + + React.useEffect(() => { + (async () => { + if (!ready && (await getItem('refreshToken')) && !currentUser) { + await getCurrentUser(); + } + setReady(true); + })(); + }, []); + return ready ? children : ; }; -export default DataRootComponent; +const mapState = ({ currentUser }: UserModuleState) => ({ + currentUser +}); + +const withConnect = connect( + mapState, + { getCurrentUser: CURRENT_USER } +); + +export default withConnect(DataRootComponent); diff --git a/modules/user/client-react/reducers/index.ts b/modules/user/client-react/reducers/index.ts index ae9007c..41b844e 100644 --- a/modules/user/client-react/reducers/index.ts +++ b/modules/user/client-react/reducers/index.ts @@ -51,7 +51,7 @@ export default function(state = defaultState, action: UserModuleActionProps) { case ActionType.SET_CURRENT_USER: return { ...state, - currentUser: action.payload.user || action.payload, + currentUser: (action.payload && action.payload.user) || action.payload, loading: false }; From e6d1d91b779af90ce229ba5a9e4285c491f98008 Mon Sep 17 00:00:00 2001 From: Ivan Isakov Date: Thu, 9 May 2019 16:46:25 +0300 Subject: [PATCH 036/104] Create react-native redux middleware --- modules/core/client-react-native/App.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/modules/core/client-react-native/App.tsx b/modules/core/client-react-native/App.tsx index 9d11b10..397f323 100644 --- a/modules/core/client-react-native/App.tsx +++ b/modules/core/client-react-native/App.tsx @@ -1,10 +1,10 @@ import React from 'react'; -import { createStore, combineReducers } from 'redux'; import { Provider } from 'react-redux'; import url from 'url'; import ClientModule from '@restapp/module-client-react-native'; import log from '../../../packages/common/log'; +import createReduxStore from '../../../packages/common/createReduxStore'; const { protocol, pathname, port } = url.parse(__API_URL__); @@ -24,13 +24,15 @@ export default class Main extends React.Component { ? `${protocol}//${url.parse(this.props.exp.manifest.bundleUrl).hostname}:${port}${pathname}` : __API_URL__; - const store = createStore( + const store = createReduxStore( Object.keys(modules.reducers).length > 0 - ? combineReducers({ + ? { ...modules.reducers - }) + } : state => state, - {} // initial state + {}, // initial state + null, + modules.httpClients ); log.info(`Connecting to REST backend at: ${apiUrl}`); From b66bfe2039527b121cdc07ceb706b6af00f21371 Mon Sep 17 00:00:00 2001 From: Ivan Isakov Date: Thu, 9 May 2019 16:46:58 +0300 Subject: [PATCH 037/104] Add react-native login --- .../components/LoginForm.native.tsx | 2 +- .../containers/DataRootComponent.native.tsx | 39 +++++++++++++++++-- .../client-react/containers/Login.native.tsx | 26 ++++++------- .../containers/Register.native.tsx | 10 ++--- .../containers/UserScreenNavigator.native.tsx | 12 +++--- modules/user/client-react/reducers/index.ts | 3 +- 6 files changed, 60 insertions(+), 32 deletions(-) diff --git a/modules/user/client-react/components/LoginForm.native.tsx b/modules/user/client-react/components/LoginForm.native.tsx index 4675a20..f27886f 100644 --- a/modules/user/client-react/components/LoginForm.native.tsx +++ b/modules/user/client-react/components/LoginForm.native.tsx @@ -143,7 +143,7 @@ const LoginFormWithFormik = withFormik { - if (e) { + if (e && e.errors) { setErrors(e.errors); } else { throw e; diff --git a/modules/user/client-react/containers/DataRootComponent.native.tsx b/modules/user/client-react/containers/DataRootComponent.native.tsx index 9684a75..160963c 100644 --- a/modules/user/client-react/containers/DataRootComponent.native.tsx +++ b/modules/user/client-react/containers/DataRootComponent.native.tsx @@ -1,9 +1,42 @@ import * as React from 'react'; +import { connect } from 'react-redux'; +import { getItem } from '@restapp/core-common/clientStorage'; + +import Loading from '../components/Loading.native'; +import { UserModuleState } from '../reducers'; +import { CURRENT_USER } from '../actions'; +import { User } from '..'; + +interface DataRootComponent { + currentUser: User; + getCurrentUser: () => void; +} + +class DataRootComponent extends React.Component { + public state = { + ready: false + }; + + public async componentDidMount() { + const { currentUser, getCurrentUser } = this.props; + if (!this.state.ready && (await getItem('refreshToken')) && !currentUser) { + await getCurrentUser(); + } + this.setState({ ready: true }); + } -class DataRootComponent extends React.Component { public render() { - return this.props.children; + return this.state.ready ? this.props.children : ; } } -export default DataRootComponent; +const mapState = ({ currentUser }: UserModuleState) => ({ + currentUser +}); + +const withConnect = connect( + mapState, + { getCurrentUser: CURRENT_USER } +); + +export default withConnect(DataRootComponent); diff --git a/modules/user/client-react/containers/Login.native.tsx b/modules/user/client-react/containers/Login.native.tsx index 40bc60c..b4d0d44 100644 --- a/modules/user/client-react/containers/Login.native.tsx +++ b/modules/user/client-react/containers/Login.native.tsx @@ -3,7 +3,6 @@ import { FormError } from '@restapp/forms-client-react'; import { translate } from '@restapp/i18n-client-react'; import authentication from '@restapp/authentication-client-react'; import { connect } from 'react-redux'; -import { compose } from 'redux'; import LoginView from '../components/LoginView.native'; import { CommonProps, LoginSubmitProps } from '../index.native'; import { LOGIN } from '../actions'; @@ -12,28 +11,25 @@ export interface LoginProps extends CommonProps { login: (values: LoginSubmitProps) => Promise | any; } -const Login = (props: LoginProps) => { - const { t, login } = props; +class Login extends React.Component { + public onSubmit = async (values: LoginSubmitProps) => { + const { t, login } = this.props; + const data = await login(values); - const onSubmit = async (values: LoginSubmitProps) => { - try { - await login(values); - } catch (e) { - throw new FormError(t('login.errorMsg'), e); + if (data && data.errors) { + throw new FormError(t('reg.errorMsg'), data); } await authentication.doLogin(); }; - - return ; -}; + public render() { + return ; + } +} const withConnect = connect( null, { login: LOGIN } ); -export default compose( - translate('user'), - withConnect(Login) -); +export default translate('user')(withConnect(Login)); diff --git a/modules/user/client-react/containers/Register.native.tsx b/modules/user/client-react/containers/Register.native.tsx index 136d277..3f99a63 100644 --- a/modules/user/client-react/containers/Register.native.tsx +++ b/modules/user/client-react/containers/Register.native.tsx @@ -1,6 +1,5 @@ import React from 'react'; import { connect } from 'react-redux'; -import { compose } from 'redux'; import { translate } from '@restapp/i18n-client-react'; import { FormError } from '@restapp/forms-client-react'; import RegisterView from '../components/RegisterView.native'; @@ -27,8 +26,8 @@ class Register extends React.Component { const data = await register(values); - if (data.error) { - throw new FormError(t('reg.errorMsg'), data.error); + if (data.errors) { + throw new FormError(t('reg.errorMsg'), data); } if (!settings.auth.password.requireEmailConfirmation) { @@ -59,7 +58,4 @@ const withConnect = connect( { register: REGISTER } ); -export default compose( - translate('user'), - withConnect(Register) -); +export default translate('user')(withConnect(Register)); diff --git a/modules/user/client-react/containers/UserScreenNavigator.native.tsx b/modules/user/client-react/containers/UserScreenNavigator.native.tsx index a15bb09..924ef4d 100644 --- a/modules/user/client-react/containers/UserScreenNavigator.native.tsx +++ b/modules/user/client-react/containers/UserScreenNavigator.native.tsx @@ -5,7 +5,7 @@ import { compose } from 'redux'; import { DrawerComponent } from '@restapp/look-client-react-native'; import { UserRole } from '../index.native'; -import { withUser } from './Auth'; +import { withUser } from './Auth.native'; interface User { id: number | string; @@ -54,7 +54,7 @@ class UserScreenNavigator extends React.Component { }; public getInitialRoute = () => { const { currentUser } = this.props; - return currentUser ? 'Profile' : 'Login'; + return currentUser ? 'Welcome' : 'Login'; }; public render() { @@ -76,13 +76,15 @@ class UserScreenNavigator extends React.Component { const drawerNavigator: any = (routeConfigs: any) => { const withRoutes = (Component: React.ComponentType) => { const ownProps = { routeConfigs }; - const WithRoutesComponent = ({ ...props }) => ; + const WithRoutesComponent = ({ ...props }) => { + return ; + }; return WithRoutesComponent; }; return compose( - withUser, - withRoutes + withRoutes, + withUser )(UserScreenNavigator); }; diff --git a/modules/user/client-react/reducers/index.ts b/modules/user/client-react/reducers/index.ts index 41b844e..a372354 100644 --- a/modules/user/client-react/reducers/index.ts +++ b/modules/user/client-react/reducers/index.ts @@ -49,9 +49,10 @@ export default function(state = defaultState, action: UserModuleActionProps) { }; case ActionType.SET_CURRENT_USER: + const currentUser = action.payload.errors ? null : (action.payload.user && action.payload) || action.payload; return { ...state, - currentUser: (action.payload && action.payload.user) || action.payload, + currentUser, loading: false }; From 97342843a9d3384e7af44c23f9dd9f84abc5117a Mon Sep 17 00:00:00 2001 From: Alexandr Belokur Date: Fri, 10 May 2019 11:00:35 +0300 Subject: [PATCH 038/104] Update errors response in login --- modules/user/server-ts/password/controllers.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/user/server-ts/password/controllers.ts b/modules/user/server-ts/password/controllers.ts index 22390e5..584360f 100644 --- a/modules/user/server-ts/password/controllers.ts +++ b/modules/user/server-ts/password/controllers.ts @@ -21,8 +21,9 @@ export const login = (req: any, res: any, next: any) => { passport.authenticate('local', { session: session.enabled }, (err, user, info) => { if (err || !user) { return res.status(400).json({ - message: info ? info.message : 'Login failed', - user + errors: { + message: info ? info.message : 'Login failed' + } }); } From 6de1a0e0105b21dab38bad9916e106e624ee5ca2 Mon Sep 17 00:00:00 2001 From: Alexandr Belokur Date: Fri, 10 May 2019 11:02:40 +0300 Subject: [PATCH 039/104] Add response for session logout --- modules/authentication/server-ts/access/session/controllers.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/authentication/server-ts/access/session/controllers.ts b/modules/authentication/server-ts/access/session/controllers.ts index 43205f6..1402e1a 100644 --- a/modules/authentication/server-ts/access/session/controllers.ts +++ b/modules/authentication/server-ts/access/session/controllers.ts @@ -2,4 +2,5 @@ import { Request, Response } from 'express'; export const logout = (req: Request, res: Response) => { req.logout(); + res.status(200).send(); }; From 92a089e9924af38dcb8daaccc2c77b28e0b1c290 Mon Sep 17 00:00:00 2001 From: Ivan Isakov Date: Fri, 10 May 2019 11:44:57 +0300 Subject: [PATCH 040/104] Fix throw --- .../authentication/client-react/access/jwt/index.ts | 2 +- modules/user/client-react/actions/register.ts | 3 +-- modules/user/client-react/containers/Login.tsx | 1 + modules/user/client-react/reducers/index.ts | 13 ++----------- 4 files changed, 5 insertions(+), 14 deletions(-) diff --git a/modules/authentication/client-react/access/jwt/index.ts b/modules/authentication/client-react/access/jwt/index.ts index 7f2c750..38b4687 100644 --- a/modules/authentication/client-react/access/jwt/index.ts +++ b/modules/authentication/client-react/access/jwt/index.ts @@ -44,7 +44,7 @@ const client = async (request: () => Promise) => { } await request(); } - return e.response; + throw e; } }; diff --git a/modules/user/client-react/actions/register.ts b/modules/user/client-react/actions/register.ts index ed7bd8b..f36043c 100644 --- a/modules/user/client-react/actions/register.ts +++ b/modules/user/client-react/actions/register.ts @@ -1,10 +1,9 @@ import axios from 'axios'; import { RegisterSubmitProps } from '..'; -import { ActionType } from '../reducers'; export default function REGISTER(value: RegisterSubmitProps) { return { - types: [null, ActionType.REGISTER, null], + types: [null, null, null] as any, callAPI: (client: (request: () => Promise) => any) => client ? client(() => axios.post(`${__API_URL__}/register`, { ...value })) diff --git a/modules/user/client-react/containers/Login.tsx b/modules/user/client-react/containers/Login.tsx index 85aa2bf..fb45fa9 100644 --- a/modules/user/client-react/containers/Login.tsx +++ b/modules/user/client-react/containers/Login.tsx @@ -40,6 +40,7 @@ const Login: React.FunctionComponent = props => { if (data && data.errors) { throw new FormError(t('reg.errorMsg'), data); } + await authentication.doLogin(); }; diff --git a/modules/user/client-react/reducers/index.ts b/modules/user/client-react/reducers/index.ts index a372354..b3e5791 100644 --- a/modules/user/client-react/reducers/index.ts +++ b/modules/user/client-react/reducers/index.ts @@ -3,14 +3,12 @@ import { User } from '..'; export enum ActionType { SET_CURRENT_USER = 'SET_CURRENT_USER', CLEAR_CURRENT_USER = 'CLEAR_CURRENT_USER', - SET_LOADING = 'SET_LOADING', - REGISTER = 'REGISTER' + SET_LOADING = 'SET_LOADING' } export interface UserModuleState { currentUser: User; loading: boolean; - register: any; } export interface UserModuleActionProps { @@ -22,18 +20,11 @@ export interface UserModuleActionProps { const defaultState: UserModuleState = { currentUser: null, - loading: false, - register: null + loading: false }; export default function(state = defaultState, action: UserModuleActionProps) { switch (action.type) { - case ActionType.REGISTER: - return { - ...state, - register: action.payload - }; - case ActionType.SET_LOADING: return { ...state, From 7f6d88659289ae570998c740a831c8931f966c6d Mon Sep 17 00:00:00 2001 From: Alexandr Belokur Date: Fri, 10 May 2019 12:25:45 +0300 Subject: [PATCH 041/104] Add controllers for profile --- modules/user/server-ts/controllers.ts | 206 +++++++++++++++++++++++++- modules/user/server-ts/index.ts | 32 +++- 2 files changed, 236 insertions(+), 2 deletions(-) diff --git a/modules/user/server-ts/controllers.ts b/modules/user/server-ts/controllers.ts index 149be28..ead8be7 100644 --- a/modules/user/server-ts/controllers.ts +++ b/modules/user/server-ts/controllers.ts @@ -1,4 +1,36 @@ -import userDAO from './sql'; +import { pick, isEmpty } from 'lodash'; + +import { createTransaction } from '@restapp/database-server-ts'; +import { log } from '@restapp/core-common'; +import { mailer } from '@restapp/mailer-server-ts'; + +import userDAO, { UserShape } from './sql'; +import settings from '../../../settings'; +import { ValidationErrors } from '.'; +import { createPasswordHash } from './password'; +import emailTemplate from './emailTemplate'; + +const { + auth: { jwt, password, secret }, + app +} = settings; + +export const user = async ({ body: { id }, user: identity, t }: any, res: any) => { + if (+identity.id === +id || identity.role === 'admin') { + try { + res.json({ user: await userDAO.getUser(id) }); + } catch (e) { + res.status(500).json({ errors: e }); + } + } else { + res.status(401).send(t('user:accessDenied')); + } +}; +export const users = async ({ body: { orderBy, filter }, user: identity, t }: any, res: any) => { + identity.role === 'admin' + ? res.json(await userDAO.getUsers(orderBy, filter)) + : res.status(401).send(t('user:accessDenied')); +}; export const currentUser = async ({ user: identity }: any, res: any) => { if (identity.id) { @@ -7,3 +39,175 @@ export const currentUser = async ({ user: identity }: any, res: any) => { res.send(null); } }; + +export const addUser = async ({ body, user: identity, t }: any, res: any) => { + if (identity.role !== 'admin') { + return res.status(401).send(t('user:accessDenied')); + } + const errors: ValidationErrors = {}; + + const userExists = await userDAO.getUserByUsername(body.username); + if (userExists) { + errors.username = t('user:usernameIsExisted'); + } + + const emailExists = await userDAO.getUserByEmail(body.email); + if (emailExists) { + errors.email = t('user:emailIsExisted'); + } + + if (body.password.length < password.minLength) { + errors.password = t('user:passwordLength'); + } + + if (!isEmpty(errors)) { + return res.status(422).json({ + errors: { + message: t('user:auth.password.registrationFailed'), + ...errors + } + }); + } + + const passwordHash: string = await createPasswordHash(body.password); + + const trx = await createTransaction(); + let createdUserId; + try { + const isActive = password.requireEmailConfirmation ? body.isActive || false : !password.requireEmailConfirmation; + + [createdUserId] = await userDAO.register({ ...body, isActive }, passwordHash).transacting(trx); + await userDAO.editUserProfile({ id: createdUserId, ...body }).transacting(trx); + trx.commit(); + } catch (e) { + res.status(500).json({ + errors: { + message: e + } + }); + trx.rollback(); + } + + try { + const createdUser = (await userDAO.getUser(createdUserId)) as UserShape; + res.json(user); + + if (mailer && password.requireEmailConfirmation && !emailExists) { + // async email + jwt.sign({ identity: pick(createdUser, 'id') }, secret, { expiresIn: '1d' }, (err: any, emailToken: string) => { + const encodedToken = Buffer.from(emailToken).toString('base64'); + const url = `${__WEBSITE_URL__}/confirmation/${encodedToken}`; + mailer.sendMail({ + from: `${app.name} <${process.env.EMAIL_USER}>`, + to: createdUser.email, + subject: 'Your account has been created', + html: emailTemplate.accountCreated(url, createdUser) + }); + log.info(`Sent registration confirmation email to: ${createdUser.email}`); + }); + } + } catch (e) { + res.status(500).json({ + errors: { + message: e + } + }); + } +}; + +export const editUser = async ({ user: identity, body, t }: any, res: any) => { + const isAdmin = () => identity.role === 'admin'; + const isSelf = () => +identity.id === +body.id; + + if (!isSelf() && !isAdmin()) { + return res.status(401).send(t('user:accessDenied')); + } + + const errors: ValidationErrors = {}; + + const userExists = (await userDAO.getUserByUsername(body.username)) as UserShape; + if (userExists && userExists.id !== body.id) { + errors.username = t('user:usernameIsExisted'); + } + + const emailExists = (await userDAO.getUserByEmail(body.email)) as UserShape; + if (emailExists && emailExists.id !== body.id) { + errors.email = t('user:emailIsExisted'); + } + + if (body.password && body.password.length < password.minLength) { + errors.password = t('user:passwordLength'); + } + + if (!isEmpty(errors)) { + return res.status(422).json({ + errors: { + message: t('user:auth.password.registrationFailed'), + ...errors + } + }); + } + + const userInfo = !isSelf() && isAdmin() ? body : pick(body, ['id', 'username', 'email', 'password']); + + const isProfileExists = await userDAO.isUserProfileExists(body.id); + const passwordHash = await createPasswordHash(body.password); + + const trx = await createTransaction(); + try { + await userDAO.editUser(userInfo, passwordHash).transacting(trx); + await userDAO.editUserProfile(body, isProfileExists).transacting(trx); + + if (mailer && body.password && password.sendPasswordChangesEmail) { + const url = `${__WEBSITE_URL__}/profile`; + + mailer.sendMail({ + from: `${settings.app.name} <${process.env.EMAIL_USER}>`, + to: body.email, + subject: 'Your Password Has Been Updated', + html: emailTemplate.passwordUpdated(url) + }); + log.info(`Sent password has been updated to: ${body.email}`); + } + trx.commit(); + } catch (e) { + res.status(500).json({ + errors: { + message: e + } + }); + trx.rollback(); + } + + try { + res.json(await userDAO.getUser(body.id)); + } catch (e) { + res.status(500).json({ + errors: { + message: e + } + }); + } +}; + +export const deleteUser = async ({ user: identity, body: { id }, t }: any, res: any) => { + const isAdmin = () => identity.role === 'admin'; + const isSelf = () => +identity.id === +id; + + const userData = await userDAO.getUser(id); + if (!userData) { + res.send(t('user:userIsNotExisted')); + } + + if (isSelf()) { + res.send(t('user:userCannotDeleteYourself')); + } + + const isDeleted = !isSelf() && isAdmin() ? await userDAO.deleteUser(id) : false; + + if (isDeleted) { + res.json(userData); + } else { + res.send(t('user:userCouldNotDeleted')); + } +}; diff --git a/modules/user/server-ts/index.ts b/modules/user/server-ts/index.ts index 2421d7f..aa5cf49 100644 --- a/modules/user/server-ts/index.ts +++ b/modules/user/server-ts/index.ts @@ -3,7 +3,7 @@ import i18n from 'i18next'; import ServerModule, { RestMethod } from '@restapp/module-server-ts'; -import { currentUser } from './controllers'; +import { user, users, currentUser, addUser, editUser, deleteUser } from './controllers'; import password from './password'; import UserDAO, { UserShape } from './sql'; import settings from '../../../settings'; @@ -52,11 +52,41 @@ export default new ServerModule(password, { appContext, localization: [{ ns: 'user', resources }], apiRouteParams: [ + { + method: RestMethod.GET, + route: 'user', + isAuthRoute: true, + controller: user + }, + { + method: RestMethod.GET, + route: 'users', + isAuthRoute: true, + controller: users + }, { method: RestMethod.GET, route: 'currentUser', isAuthRoute: true, controller: currentUser + }, + { + method: RestMethod.POST, + route: 'addUser', + isAuthRoute: true, + controller: addUser + }, + { + method: RestMethod.POST, + route: 'editUser', + isAuthRoute: true, + controller: editUser + }, + { + method: RestMethod.DELETE, + route: 'deleteUser', + isAuthRoute: true, + controller: deleteUser } ] }); From 8d867e105371e1b83760a7c05bcc040bd882c300 Mon Sep 17 00:00:00 2001 From: Ivan Isakov Date: Fri, 10 May 2019 17:52:25 +0300 Subject: [PATCH 042/104] Move redux middleware in authentication jwt module and add requests for profile data --- .../client-react/access/jwt/index.ts | 43 ++++++++++++++++- modules/core/client-react-native/App.tsx | 2 +- modules/core/client-react/Main.tsx | 2 +- modules/core/common/createReduxStore.ts | 47 +++---------------- modules/module/client-react/ClientModule.ts | 7 +-- modules/user/client-react/actions/addUser.ts | 10 ++++ .../user/client-react/actions/currentUser.ts | 3 +- .../user/client-react/actions/deleteUser.ts | 9 ++++ modules/user/client-react/actions/editUser.ts | 9 ++++ modules/user/client-react/actions/index.ts | 7 ++- modules/user/client-react/actions/login.ts | 5 +- modules/user/client-react/actions/register.ts | 5 +- modules/user/client-react/actions/user.ts | 9 ++++ modules/user/client-react/actions/users.ts | 11 +++++ .../client-react/helpers/UserFormatter.ts | 24 ++++++++++ modules/user/client-react/helpers/index.ts | 13 +++++ modules/user/client-react/reducers/index.ts | 41 ++++++++++++++-- packages/client/typings/typings.d.ts | 1 + 18 files changed, 187 insertions(+), 61 deletions(-) create mode 100644 modules/user/client-react/actions/addUser.ts create mode 100644 modules/user/client-react/actions/deleteUser.ts create mode 100644 modules/user/client-react/actions/editUser.ts create mode 100644 modules/user/client-react/actions/user.ts create mode 100644 modules/user/client-react/actions/users.ts create mode 100644 modules/user/client-react/helpers/UserFormatter.ts create mode 100644 modules/user/client-react/helpers/index.ts diff --git a/modules/authentication/client-react/access/jwt/index.ts b/modules/authentication/client-react/access/jwt/index.ts index 38b4687..7cea00f 100644 --- a/modules/authentication/client-react/access/jwt/index.ts +++ b/modules/authentication/client-react/access/jwt/index.ts @@ -1,5 +1,6 @@ import axios from 'axios'; import { getItem, setItem, removeItem } from '@restapp/core-common/clientStorage'; +import { Middleware } from 'redux'; import settings from '../../../../../settings'; import AccessModule from '../AccessModule'; @@ -22,6 +23,44 @@ const removeTokens = async () => { await removeItem(TokensEnum.refreshToken); }; +const requestMiddleware: Middleware = ({ dispatch }) => next => action => { + const { types, callAPI, payload, ...rest } = action; + + if (!types) { + return next(action); + } + + const [REQUEST, SUCCESS, FAIL] = types; + + next({ type: REQUEST, payload, ...rest }); + + const handleCallApi = async () => { + try { + const result = await client(callAPI); + const data = result && result.data; + next({ + type: SUCCESS, + payload: data, + ...rest + }); + return data; + } catch (e) { + if (e.response.status === 401) { + dispatch(action); + } + const data = e.response && e.response.data; + next({ + type: FAIL, + payload: data, + ...rest + }); + return data; + } + }; + + return handleCallApi(); +}; + const client = async (request: () => Promise) => { try { const result = await request(); @@ -42,7 +81,6 @@ const client = async (request: () => Promise) => { await removeTokens(); throw e; } - await request(); } throw e; } @@ -67,6 +105,7 @@ axios.interceptors.response.use(async (res: any) => { } } = res; await saveTokens({ accessToken, refreshToken }); + return res.data.user; } else { await removeTokens(); } @@ -77,6 +116,6 @@ axios.interceptors.response.use(async (res: any) => { export default (settings.auth.jwt.enabled ? new AccessModule({ logout: [removeTokens], - httpClient: client + requestMiddleware }) : undefined); diff --git a/modules/core/client-react-native/App.tsx b/modules/core/client-react-native/App.tsx index 397f323..31b35aa 100644 --- a/modules/core/client-react-native/App.tsx +++ b/modules/core/client-react-native/App.tsx @@ -32,7 +32,7 @@ export default class Main extends React.Component { : state => state, {}, // initial state null, - modules.httpClients + modules.requestMiddlewares ); log.info(`Connecting to REST backend at: ${apiUrl}`); diff --git a/modules/core/client-react/Main.tsx b/modules/core/client-react/Main.tsx index 1a5aca9..9221881 100644 --- a/modules/core/client-react/Main.tsx +++ b/modules/core/client-react/Main.tsx @@ -25,7 +25,7 @@ export const onAppCreate = (modules: ClientModule, entryModule: NodeModule) => { ref.store = entryModule.hot.data.store; ref.store.replaceReducer(getStoreReducer(ref.modules.reducers)); } else { - ref.store = createReduxStore(ref.modules.reducers, {}, routerMiddleware(history), ref.modules.httpClients); + ref.store = createReduxStore(ref.modules.reducers, {}, routerMiddleware(history), ref.modules.requestMiddlewares); } }; diff --git a/modules/core/common/createReduxStore.ts b/modules/core/common/createReduxStore.ts index 825d0a2..c985026 100644 --- a/modules/core/common/createReduxStore.ts +++ b/modules/core/common/createReduxStore.ts @@ -8,54 +8,21 @@ export const getStoreReducer = (reducers: any) => ...reducers }); -const requestMiddleware: (httpClient?: any) => Middleware = httpClient => _state => next => action => { - const { types, callAPI, ...rest } = action; - - if (!types) { - return next(action); - } - - const [REQUEST, SUCCESS, FAIL] = types; - - next({ type: REQUEST, ...rest }); - - const handleCallApi = async () => { - try { - const result = await callAPI(httpClient); - const data = result && result.data; - next({ - type: SUCCESS, - payload: data, - ...rest - }); - return data; - } catch (e) { - const data = e.response && e.response.data; - next({ - type: FAIL, - payload: data, - ...rest - }); - return data; - } - }; - - return handleCallApi(); -}; - const createReduxStore = ( reducers: Reducer, initialState: DeepPartial, routerMiddleware?: Middleware, - httpClient?: (request: () => Promise) => void + requestMiddleware?: Middleware ): Store => { - const middleware = routerMiddleware - ? [routerMiddleware, requestMiddleware(httpClient)] - : [requestMiddleware(httpClient)]; + const middleware: () => Middleware[] = () => { + const routerMiddlewares = routerMiddleware ? [routerMiddleware] : []; + const requestMiddlewares = requestMiddleware ? [requestMiddleware] : []; + return [...routerMiddlewares, ...requestMiddlewares]; + }; return createStore( getStoreReducer(reducers), initialState, // initial state, - composeWithDevTools(applyMiddleware(...middleware)) + composeWithDevTools(applyMiddleware(...middleware())) ); }; diff --git a/modules/module/client-react/ClientModule.ts b/modules/module/client-react/ClientModule.ts index 236a297..f42940b 100644 --- a/modules/module/client-react/ClientModule.ts +++ b/modules/module/client-react/ClientModule.ts @@ -1,5 +1,6 @@ import React from 'react'; import BaseModule, { BaseModuleShape } from './BaseModule'; +import { Middleware } from 'redux'; /** * React client feature modules interface. @@ -16,7 +17,7 @@ export interface ClientModuleShape extends BaseModuleShape { // URL list to 3rd party js scripts scriptsInsert?: string[]; // Http client is provided by modules - httpClient?: (request: () => Promise) => void; + requestMiddleware?: Middleware; } interface ClientModule extends ClientModuleShape {} @@ -88,8 +89,8 @@ class ClientModule extends BaseModule { /** * @returns Http client is provided by modules */ - get httpClients() { - return this.httpClient; + get requestMiddlewares() { + return this.requestMiddleware; } } diff --git a/modules/user/client-react/actions/addUser.ts b/modules/user/client-react/actions/addUser.ts new file mode 100644 index 0000000..a1e3082 --- /dev/null +++ b/modules/user/client-react/actions/addUser.ts @@ -0,0 +1,10 @@ +import axios from 'axios'; +import { ActionType } from '../reducers'; +import { User } from '..'; + +export default function ADD_USER(user: User) { + return { + types: [null, ActionType.SET_CURRENT_USER, ActionType.CLEAR_CURRENT_USER], + callAPI: () => axios.post(`${__API_URL__}/addUser`, { ...user }) + }; +} diff --git a/modules/user/client-react/actions/currentUser.ts b/modules/user/client-react/actions/currentUser.ts index 87dea99..dab1a55 100644 --- a/modules/user/client-react/actions/currentUser.ts +++ b/modules/user/client-react/actions/currentUser.ts @@ -4,7 +4,6 @@ import { ActionType } from '../reducers'; export default function CURRENT_USER() { return { types: [null, ActionType.SET_CURRENT_USER, ActionType.CLEAR_CURRENT_USER], - callAPI: (client: (request: () => Promise) => any) => - client ? client(() => axios.get(`${__API_URL__}/currentUser`)) : axios.get(`${__API_URL__}/currentUser`) + callAPI: () => axios.get(`${__API_URL__}/currentUser`) }; } diff --git a/modules/user/client-react/actions/deleteUser.ts b/modules/user/client-react/actions/deleteUser.ts new file mode 100644 index 0000000..0c6a2a1 --- /dev/null +++ b/modules/user/client-react/actions/deleteUser.ts @@ -0,0 +1,9 @@ +import axios from 'axios'; +import { ActionType } from '../reducers'; + +export default function DELETE_USER(id: number) { + return { + types: [null, ActionType.SET_CURRENT_USER, ActionType.CLEAR_CURRENT_USER], + callAPI: () => axios.delete(`${__API_URL__}/deleteUser`, { data: id }) + }; +} diff --git a/modules/user/client-react/actions/editUser.ts b/modules/user/client-react/actions/editUser.ts new file mode 100644 index 0000000..17300ff --- /dev/null +++ b/modules/user/client-react/actions/editUser.ts @@ -0,0 +1,9 @@ +import axios from 'axios'; +import { User } from '..'; + +export default function EDIT_USER(user: User) { + return { + types: [null, null, null] as any, + callAPI: () => axios.post(`${__API_URL__}/editUser`, { ...user }) + }; +} diff --git a/modules/user/client-react/actions/index.ts b/modules/user/client-react/actions/index.ts index 6e9ce09..7dace9f 100644 --- a/modules/user/client-react/actions/index.ts +++ b/modules/user/client-react/actions/index.ts @@ -2,5 +2,10 @@ import REGISTER from './register'; import LOGIN from './login'; import CURRENT_USER from './currentUser'; import CLEAR_USER from './clearUser'; +import USERS from './users'; +import USER from './user'; +import DELETE_USER from './deleteUser'; +import EDIT_USER from './editUser'; +import ADD_USER from './addUser'; -export { REGISTER, LOGIN, CURRENT_USER, CLEAR_USER }; +export { REGISTER, LOGIN, CURRENT_USER, CLEAR_USER, USERS, USER, DELETE_USER, EDIT_USER, ADD_USER }; diff --git a/modules/user/client-react/actions/login.ts b/modules/user/client-react/actions/login.ts index 5dcbfb2..d0c5544 100644 --- a/modules/user/client-react/actions/login.ts +++ b/modules/user/client-react/actions/login.ts @@ -5,9 +5,6 @@ import { ActionType } from '../reducers'; export default function LOGIN(value: LoginSubmitProps) { return { types: [null, ActionType.SET_CURRENT_USER, ActionType.CLEAR_CURRENT_USER], - callAPI: (client: (request: () => Promise) => any) => - client - ? client(() => axios.post(`${__API_URL__}/login`, { ...value })) - : axios.post(`${__API_URL__}/login`, { ...value }) + callAPI: () => axios.post(`${__API_URL__}/login`, { ...value }) }; } diff --git a/modules/user/client-react/actions/register.ts b/modules/user/client-react/actions/register.ts index f36043c..461a872 100644 --- a/modules/user/client-react/actions/register.ts +++ b/modules/user/client-react/actions/register.ts @@ -4,9 +4,6 @@ import { RegisterSubmitProps } from '..'; export default function REGISTER(value: RegisterSubmitProps) { return { types: [null, null, null] as any, - callAPI: (client: (request: () => Promise) => any) => - client - ? client(() => axios.post(`${__API_URL__}/register`, { ...value })) - : axios.post(`${__API_URL__}/register`, { ...value }) + callAPI: () => axios.post(`${__API_URL__}/register`, { ...value }) }; } diff --git a/modules/user/client-react/actions/user.ts b/modules/user/client-react/actions/user.ts new file mode 100644 index 0000000..7282299 --- /dev/null +++ b/modules/user/client-react/actions/user.ts @@ -0,0 +1,9 @@ +import axios from 'axios'; +import { ActionType } from '../reducers'; + +export default function USER(id: number) { + return { + types: [null, ActionType.SET_USER, null], + callAPI: () => axios.get(`${__API_URL__}/user`, { params: id }) + }; +} diff --git a/modules/user/client-react/actions/users.ts b/modules/user/client-react/actions/users.ts new file mode 100644 index 0000000..b9c0dfe --- /dev/null +++ b/modules/user/client-react/actions/users.ts @@ -0,0 +1,11 @@ +import axios from 'axios'; +import { ActionType } from '../reducers'; +import { OrderBy, Filter } from '..'; + +export default function USERS(orderBY: OrderBy, filter: Filter, type = 'SET_LOADING') { + return { + types: [ActionType[type], ActionType.SET_USERS, null], + payload: { orderBY, filter }, + callAPI: () => axios.get(`${__API_URL__}/users`, { params: { filter, orderBY } }) + }; +} diff --git a/modules/user/client-react/helpers/UserFormatter.ts b/modules/user/client-react/helpers/UserFormatter.ts new file mode 100644 index 0000000..f0adf09 --- /dev/null +++ b/modules/user/client-react/helpers/UserFormatter.ts @@ -0,0 +1,24 @@ +export default class UserFormatter { + public static trimExtraSpaces(inputValues: { [key: string]: any }) { + const userValues = { ...inputValues }; + const propsForTrim = ['username', 'email', 'firstName', 'lastName']; + + for (const prop in userValues) { + if (userValues.hasOwnProperty(prop)) { + if (propsForTrim.includes(prop) && userValues[prop]) { + userValues[prop] = userValues[prop].trim(); + } + + if (prop === 'profile') { + for (const profileProp in userValues.profile) { + if (propsForTrim.includes(profileProp) && userValues.profile[profileProp]) { + userValues.profile[profileProp] = userValues.profile[profileProp].trim(); + } + } + } + } + } + + return userValues; + } +} diff --git a/modules/user/client-react/helpers/index.ts b/modules/user/client-react/helpers/index.ts new file mode 100644 index 0000000..3578675 --- /dev/null +++ b/modules/user/client-react/helpers/index.ts @@ -0,0 +1,13 @@ +import url from 'url'; +import { Constants } from 'expo'; +import { Platform } from 'react-native'; + +export default function buildRedirectUrlForMobile(authType: string) { + const { protocol, hostname, port } = url.parse(__WEBSITE_URL__); + const expoHostname = Platform.OS === 'android' ? 'localhost' : `${url.parse(Constants.linkingUri).hostname}.nip.io`; + const urlHostname = process.env.NODE_ENV === 'production' ? hostname : expoHostname; + + return `${protocol}//${urlHostname}${port ? ':' + port : ''}/auth/${authType}?expoUrl=${encodeURIComponent( + Constants.linkingUri + )}`; +} diff --git a/modules/user/client-react/reducers/index.ts b/modules/user/client-react/reducers/index.ts index b3e5791..7a015d0 100644 --- a/modules/user/client-react/reducers/index.ts +++ b/modules/user/client-react/reducers/index.ts @@ -1,14 +1,22 @@ -import { User } from '..'; +import { User, OrderBy, Filter } from '..'; export enum ActionType { SET_CURRENT_USER = 'SET_CURRENT_USER', CLEAR_CURRENT_USER = 'CLEAR_CURRENT_USER', - SET_LOADING = 'SET_LOADING' + SET_LOADING = 'SET_LOADING', + SET_USER = 'SET_USER', + SET_USERS = 'SET_USERS', + SET_ORDER_BY = 'SET_ORDER_BY', + SET_FILTER = 'SET_FILTER' } export interface UserModuleState { currentUser: User; loading: boolean; + user: User; + users: User[]; + orderBy: OrderBy; + filter: Filter; } export interface UserModuleActionProps { @@ -20,7 +28,11 @@ export interface UserModuleActionProps { const defaultState: UserModuleState = { currentUser: null, - loading: false + loading: false, + user: null, + users: [], + orderBy: { column: '', order: '' }, + filter: { searchText: '', role: null, isActive: true } }; export default function(state = defaultState, action: UserModuleActionProps) { @@ -47,6 +59,29 @@ export default function(state = defaultState, action: UserModuleActionProps) { loading: false }; + case ActionType.SET_USER: + return { + ...state, + user: action.payload + }; + case ActionType.SET_USERS: + return { + ...state, + users: action.payload + }; + + case ActionType.SET_FILTER: + return { + ...state, + filter: action.payload && action.payload.filter + }; + + case ActionType.SET_ORDER_BY: + return { + ...state, + orderBy: action.payload && action.payload.orderBy + }; + default: return state; } diff --git a/packages/client/typings/typings.d.ts b/packages/client/typings/typings.d.ts index 9f68cd8..bb8bb24 100644 --- a/packages/client/typings/typings.d.ts +++ b/packages/client/typings/typings.d.ts @@ -13,3 +13,4 @@ interface Window { declare module 'react-native-credit-card-input'; declare module 'sourcemapped-stacktrace'; declare module 'minilog'; +declare module 'reactstrap'; From eded36da9f5ca003b1644cf21538d2d2d22a2062 Mon Sep 17 00:00:00 2001 From: Alexandr Belokur Date: Mon, 13 May 2019 11:02:19 +0300 Subject: [PATCH 043/104] Update controllers --- config/auth.js | 2 +- modules/user/server-ts/controllers.ts | 5 +++-- modules/user/server-ts/index.ts | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/config/auth.js b/config/auth.js index 61fe596..5371613 100644 --- a/config/auth.js +++ b/config/auth.js @@ -4,7 +4,7 @@ export default { enabled: true }, jwt: { - enabled: true, + enabled: false, tokenExpiresIn: '1m', refreshTokenExpiresIn: '7d' }, diff --git a/modules/user/server-ts/controllers.ts b/modules/user/server-ts/controllers.ts index ead8be7..c139a1e 100644 --- a/modules/user/server-ts/controllers.ts +++ b/modules/user/server-ts/controllers.ts @@ -1,4 +1,5 @@ import { pick, isEmpty } from 'lodash'; +import jwt from 'jsonwebtoken'; import { createTransaction } from '@restapp/database-server-ts'; import { log } from '@restapp/core-common'; @@ -11,11 +12,11 @@ import { createPasswordHash } from './password'; import emailTemplate from './emailTemplate'; const { - auth: { jwt, password, secret }, + auth: { password, secret }, app } = settings; -export const user = async ({ body: { id }, user: identity, t }: any, res: any) => { +export const user = async ({ params: { id }, user: identity, t }: any, res: any) => { if (+identity.id === +id || identity.role === 'admin') { try { res.json({ user: await userDAO.getUser(id) }); diff --git a/modules/user/server-ts/index.ts b/modules/user/server-ts/index.ts index aa5cf49..8f675ed 100644 --- a/modules/user/server-ts/index.ts +++ b/modules/user/server-ts/index.ts @@ -54,12 +54,12 @@ export default new ServerModule(password, { apiRouteParams: [ { method: RestMethod.GET, - route: 'user', + route: 'user/:id', isAuthRoute: true, controller: user }, { - method: RestMethod.GET, + method: RestMethod.POST, route: 'users', isAuthRoute: true, controller: users From 7f69b5c5a5f11b013cc6f1cba9d2a0b26e41a87a Mon Sep 17 00:00:00 2001 From: Ivan Isakov Date: Mon, 13 May 2019 15:55:01 +0300 Subject: [PATCH 044/104] Rename redux middleware --- config/auth.js | 2 +- .../client-react/access/jwt/index.ts | 24 +++++++++---------- modules/core/client-react/Main.tsx | 2 +- modules/core/common/createReduxStore.ts | 7 +++--- modules/module/client-react/ClientModule.ts | 6 ++--- 5 files changed, 21 insertions(+), 20 deletions(-) diff --git a/config/auth.js b/config/auth.js index 5371613..61fe596 100644 --- a/config/auth.js +++ b/config/auth.js @@ -4,7 +4,7 @@ export default { enabled: true }, jwt: { - enabled: false, + enabled: true, tokenExpiresIn: '1m', refreshTokenExpiresIn: '7d' }, diff --git a/modules/authentication/client-react/access/jwt/index.ts b/modules/authentication/client-react/access/jwt/index.ts index 7cea00f..d09af1e 100644 --- a/modules/authentication/client-react/access/jwt/index.ts +++ b/modules/authentication/client-react/access/jwt/index.ts @@ -24,7 +24,7 @@ const removeTokens = async () => { }; const requestMiddleware: Middleware = ({ dispatch }) => next => action => { - const { types, callAPI, payload, ...rest } = action; + const { types, callAPI, ...rest } = action; if (!types) { return next(action); @@ -32,7 +32,7 @@ const requestMiddleware: Middleware = ({ dispatch }) => next => action => { const [REQUEST, SUCCESS, FAIL] = types; - next({ type: REQUEST, payload, ...rest }); + next({ type: REQUEST, ...rest }); const handleCallApi = async () => { try { @@ -40,19 +40,19 @@ const requestMiddleware: Middleware = ({ dispatch }) => next => action => { const data = result && result.data; next({ type: SUCCESS, - payload: data, - ...rest + ...rest, + payload: data }); return data; } catch (e) { - if (e.response.status === 401) { - dispatch(action); + if (e.response && e.response.status === 401) { + return dispatch(action); } const data = e.response && e.response.data; next({ type: FAIL, - payload: data, - ...rest + ...rest, + payload: data }); return data; } @@ -63,8 +63,7 @@ const requestMiddleware: Middleware = ({ dispatch }) => next => action => { const client = async (request: () => Promise) => { try { - const result = await request(); - return result; + return await request(); } catch (e) { if (e.response && e.response.status === 401) { try { @@ -105,7 +104,8 @@ axios.interceptors.response.use(async (res: any) => { } } = res; await saveTokens({ accessToken, refreshToken }); - return res.data.user; + res.data = res.data.user; + return res; } else { await removeTokens(); } @@ -116,6 +116,6 @@ axios.interceptors.response.use(async (res: any) => { export default (settings.auth.jwt.enabled ? new AccessModule({ logout: [removeTokens], - requestMiddleware + reduxMiddleware: [requestMiddleware] }) : undefined); diff --git a/modules/core/client-react/Main.tsx b/modules/core/client-react/Main.tsx index 9221881..479b27d 100644 --- a/modules/core/client-react/Main.tsx +++ b/modules/core/client-react/Main.tsx @@ -25,7 +25,7 @@ export const onAppCreate = (modules: ClientModule, entryModule: NodeModule) => { ref.store = entryModule.hot.data.store; ref.store.replaceReducer(getStoreReducer(ref.modules.reducers)); } else { - ref.store = createReduxStore(ref.modules.reducers, {}, routerMiddleware(history), ref.modules.requestMiddlewares); + ref.store = createReduxStore(ref.modules.reducers, {}, routerMiddleware(history), ref.modules.reduxMiddlewares); } }; diff --git a/modules/core/common/createReduxStore.ts b/modules/core/common/createReduxStore.ts index c985026..643425e 100644 --- a/modules/core/common/createReduxStore.ts +++ b/modules/core/common/createReduxStore.ts @@ -12,12 +12,13 @@ const createReduxStore = ( reducers: Reducer, initialState: DeepPartial, routerMiddleware?: Middleware, - requestMiddleware?: Middleware + reduxMiddlewares?: Middleware[] ): Store => { const middleware: () => Middleware[] = () => { const routerMiddlewares = routerMiddleware ? [routerMiddleware] : []; - const requestMiddlewares = requestMiddleware ? [requestMiddleware] : []; - return [...routerMiddlewares, ...requestMiddlewares]; + const reduxMiddleware = reduxMiddlewares && reduxMiddlewares.length ? reduxMiddlewares : []; + + return [...routerMiddlewares, ...reduxMiddleware]; }; return createStore( getStoreReducer(reducers), diff --git a/modules/module/client-react/ClientModule.ts b/modules/module/client-react/ClientModule.ts index f42940b..f19cb31 100644 --- a/modules/module/client-react/ClientModule.ts +++ b/modules/module/client-react/ClientModule.ts @@ -17,7 +17,7 @@ export interface ClientModuleShape extends BaseModuleShape { // URL list to 3rd party js scripts scriptsInsert?: string[]; // Http client is provided by modules - requestMiddleware?: Middleware; + reduxMiddleware?: Middleware[]; } interface ClientModule extends ClientModuleShape {} @@ -89,8 +89,8 @@ class ClientModule extends BaseModule { /** * @returns Http client is provided by modules */ - get requestMiddlewares() { - return this.requestMiddleware; + get reduxMiddlewares() { + return this.reduxMiddleware; } } From 4bb5ebbab2d0cf8e14f6cba59c85c97a6d312add Mon Sep 17 00:00:00 2001 From: Ivan Isakov Date: Mon, 13 May 2019 15:55:22 +0300 Subject: [PATCH 045/104] Fix get users and users filters --- .../components/{FormItem.jsx => FormItem.tsx} | 13 +- .../ui-bootstrap/components/Input.jsx | 13 -- .../ui-bootstrap/components/Input.tsx | 12 ++ .../components/{Select.jsx => Select.tsx} | 11 +- .../user/client-react/actions/deleteUser.ts | 4 +- modules/user/client-react/actions/user.ts | 2 +- modules/user/client-react/actions/users.ts | 6 +- .../client-react/components/LoginForm.tsx | 2 +- .../client-react/components/ProfileView.tsx | 80 +++++++++ .../client-react/components/UserAddView.tsx | 45 +++++ .../client-react/components/UserEditView.tsx | 61 +++++++ .../user/client-react/components/UserForm.tsx | 165 ++++++++++++++++++ .../components/UsersFilterView.tsx | 57 ++++++ .../client-react/components/UsersListView.tsx | 131 ++++++++++++++ .../user/client-react/containers/Profile.tsx | 21 +++ .../user/client-react/containers/UserAdd.tsx | 62 +++++++ .../user/client-react/containers/UserEdit.tsx | 69 ++++++++ .../containers/UserOperations.tsx | 69 ++++++++ .../user/client-react/containers/Users.tsx | 49 ++++++ modules/user/client-react/index.tsx | 40 ++++- modules/user/client-react/reducers/index.ts | 13 +- 21 files changed, 886 insertions(+), 39 deletions(-) rename modules/look/client-react/ui-bootstrap/components/{FormItem.jsx => FormItem.tsx} (62%) delete mode 100644 modules/look/client-react/ui-bootstrap/components/Input.jsx create mode 100644 modules/look/client-react/ui-bootstrap/components/Input.tsx rename modules/look/client-react/ui-bootstrap/components/{Select.jsx => Select.tsx} (51%) create mode 100644 modules/user/client-react/components/ProfileView.tsx create mode 100644 modules/user/client-react/components/UserAddView.tsx create mode 100644 modules/user/client-react/components/UserEditView.tsx create mode 100644 modules/user/client-react/components/UserForm.tsx create mode 100644 modules/user/client-react/components/UsersFilterView.tsx create mode 100644 modules/user/client-react/components/UsersListView.tsx create mode 100644 modules/user/client-react/containers/Profile.tsx create mode 100644 modules/user/client-react/containers/UserAdd.tsx create mode 100644 modules/user/client-react/containers/UserEdit.tsx create mode 100644 modules/user/client-react/containers/UserOperations.tsx create mode 100644 modules/user/client-react/containers/Users.tsx diff --git a/modules/look/client-react/ui-bootstrap/components/FormItem.jsx b/modules/look/client-react/ui-bootstrap/components/FormItem.tsx similarity index 62% rename from modules/look/client-react/ui-bootstrap/components/FormItem.jsx rename to modules/look/client-react/ui-bootstrap/components/FormItem.tsx index 16acdaa..71b38ec 100644 --- a/modules/look/client-react/ui-bootstrap/components/FormItem.jsx +++ b/modules/look/client-react/ui-bootstrap/components/FormItem.tsx @@ -1,8 +1,12 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { FormGroup, Label } from 'reactstrap'; -const FormItem = ({ children, label, ...props }) => { +interface FormItemProps { + children: React.ReactNode; + label?: string; +} + +const FormItem = ({ children, label, ...props }: FormItemProps) => { return ( {label && ( @@ -16,9 +20,4 @@ const FormItem = ({ children, label, ...props }) => { ); }; -FormItem.propTypes = { - children: PropTypes.node, - label: PropTypes.string -}; - export default FormItem; diff --git a/modules/look/client-react/ui-bootstrap/components/Input.jsx b/modules/look/client-react/ui-bootstrap/components/Input.jsx deleted file mode 100644 index e51b62b..0000000 --- a/modules/look/client-react/ui-bootstrap/components/Input.jsx +++ /dev/null @@ -1,13 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { Input as RSInput } from 'reactstrap'; - -const Input = ({ children, ...props }) => { - return {children}; -}; - -Input.propTypes = { - children: PropTypes.node -}; - -export default Input; diff --git a/modules/look/client-react/ui-bootstrap/components/Input.tsx b/modules/look/client-react/ui-bootstrap/components/Input.tsx new file mode 100644 index 0000000..b76daab --- /dev/null +++ b/modules/look/client-react/ui-bootstrap/components/Input.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { Input as RSInput } from 'reactstrap'; + +interface InputProps extends React.InputHTMLAttributes { + children?: React.ReactNode; +} + +const Input = ({ children, ...props }: InputProps) => { + return {children}; +}; + +export default Input; diff --git a/modules/look/client-react/ui-bootstrap/components/Select.jsx b/modules/look/client-react/ui-bootstrap/components/Select.tsx similarity index 51% rename from modules/look/client-react/ui-bootstrap/components/Select.jsx rename to modules/look/client-react/ui-bootstrap/components/Select.tsx index 39d62bc..c1df135 100644 --- a/modules/look/client-react/ui-bootstrap/components/Select.jsx +++ b/modules/look/client-react/ui-bootstrap/components/Select.tsx @@ -1,8 +1,11 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { Input } from 'reactstrap'; -const Select = ({ children, ...props }) => { +interface SelectProps extends React.InputHTMLAttributes { + children?: React.ReactNode; +} + +const Select = ({ children, ...props }: SelectProps) => { return ( {children} @@ -10,8 +13,4 @@ const Select = ({ children, ...props }) => { ); }; -Select.propTypes = { - children: PropTypes.node -}; - export default Select; diff --git a/modules/user/client-react/actions/deleteUser.ts b/modules/user/client-react/actions/deleteUser.ts index 0c6a2a1..3a9a2d9 100644 --- a/modules/user/client-react/actions/deleteUser.ts +++ b/modules/user/client-react/actions/deleteUser.ts @@ -3,7 +3,7 @@ import { ActionType } from '../reducers'; export default function DELETE_USER(id: number) { return { - types: [null, ActionType.SET_CURRENT_USER, ActionType.CLEAR_CURRENT_USER], - callAPI: () => axios.delete(`${__API_URL__}/deleteUser`, { data: id }) + types: [null, ActionType.DELETE_USER, null] as ActionType[], + callAPI: () => axios.delete(`${__API_URL__}/deleteUser`, { data: { id } }) }; } diff --git a/modules/user/client-react/actions/user.ts b/modules/user/client-react/actions/user.ts index 7282299..f6754b1 100644 --- a/modules/user/client-react/actions/user.ts +++ b/modules/user/client-react/actions/user.ts @@ -4,6 +4,6 @@ import { ActionType } from '../reducers'; export default function USER(id: number) { return { types: [null, ActionType.SET_USER, null], - callAPI: () => axios.get(`${__API_URL__}/user`, { params: id }) + callAPI: () => axios.get(`${__API_URL__}/user/${id}`) }; } diff --git a/modules/user/client-react/actions/users.ts b/modules/user/client-react/actions/users.ts index b9c0dfe..2fa3b2a 100644 --- a/modules/user/client-react/actions/users.ts +++ b/modules/user/client-react/actions/users.ts @@ -2,10 +2,10 @@ import axios from 'axios'; import { ActionType } from '../reducers'; import { OrderBy, Filter } from '..'; -export default function USERS(orderBY: OrderBy, filter: Filter, type = 'SET_LOADING') { +export default function USERS(orderBy: OrderBy, filter: Filter, type = 'null') { return { types: [ActionType[type], ActionType.SET_USERS, null], - payload: { orderBY, filter }, - callAPI: () => axios.get(`${__API_URL__}/users`, { params: { filter, orderBY } }) + payload: { orderBy, filter }, + callAPI: () => axios.post(`${__API_URL__}/users`, { filter, orderBy }) }; } diff --git a/modules/user/client-react/components/LoginForm.tsx b/modules/user/client-react/components/LoginForm.tsx index 5084a55..d7845bc 100644 --- a/modules/user/client-react/components/LoginForm.tsx +++ b/modules/user/client-react/components/LoginForm.tsx @@ -101,7 +101,7 @@ const LoginFormWithFormik = withFormik, LoginSubmitP handleSubmit(values, { setErrors, props: { onSubmit } }) { onSubmit(values).catch((e: any) => { - if (e) { + if (e && e.errors) { setErrors(e.errors); } else { throw e; diff --git a/modules/user/client-react/components/ProfileView.tsx b/modules/user/client-react/components/ProfileView.tsx new file mode 100644 index 0000000..04dab1f --- /dev/null +++ b/modules/user/client-react/components/ProfileView.tsx @@ -0,0 +1,80 @@ +import * as React from 'react'; +import Helmet from 'react-helmet'; +import { Link } from 'react-router-dom'; +import { translate, TranslateFunction } from '@restapp/i18n-client-react'; +import { LayoutCenter, Card, CardGroup, CardTitle, CardText, PageLayout } from '@restapp/look-client-react'; + +import { User } from '..'; + +import settings from '../../../../settings'; + +interface ProfileViewProps { + currentUserLoading: boolean; + currentUser: User; + t: TranslateFunction; +} +const renderMetaData = (t: TranslateFunction) => { + return ( + + ); +}; + +const ProfileView: React.FunctionComponent = ({ currentUserLoading, currentUser, t }) => { + if (currentUserLoading && !currentUser) { + return ( + + {renderMetaData(t)} +
{t('profile.loadMsg')}
+
+ ); + } else if (currentUser) { + return ( + + {renderMetaData(t)} + +

{t('profile.card.title')}

+ + + {t('profile.card.group.name')}: + {currentUser.username} + + + {t('profile.card.group.email')}: + {currentUser.email} + + + {t('profile.card.group.role')}: + {currentUser.role} + + {currentUser.profile && currentUser.profile.fullName && ( + + {t('profile.card.group.full')}: + {currentUser.profile.fullName} + + )} + + + {t('profile.editProfileText')} + +
+
+ ); + } else { + return ( + + {renderMetaData(t)} +

{t('profile.errorMsg')}

+
+ ); + } +}; + +export default translate('user')(ProfileView); diff --git a/modules/user/client-react/components/UserAddView.tsx b/modules/user/client-react/components/UserAddView.tsx new file mode 100644 index 0000000..11435f9 --- /dev/null +++ b/modules/user/client-react/components/UserAddView.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import Helmet from 'react-helmet'; +import { Link } from 'react-router-dom'; +import { translate, TranslateFunction } from '@restapp/i18n-client-react'; +import { PageLayout } from '@restapp/look-client-react'; + +import UserForm from './UserForm'; +import settings from '../../../../settings'; +import { User, ResetPasswordSubmitProps } from '..'; + +interface FormValues extends User, ResetPasswordSubmitProps {} + +interface UserAddViewProps { + t: TranslateFunction; + onSubmit: (values: FormValues) => Promise; +} + +const UserAddView = ({ t, onSubmit }: UserAddViewProps) => { + const renderMetaData = () => ( + + ); + + return ( + + {renderMetaData()} + + Back + +

+ {t('userEdit.form.titleCreate')} {t('userEdit.form.title')} +

+ +
+ ); +}; + +export default translate('user')(UserAddView); diff --git a/modules/user/client-react/components/UserEditView.tsx b/modules/user/client-react/components/UserEditView.tsx new file mode 100644 index 0000000..7995aeb --- /dev/null +++ b/modules/user/client-react/components/UserEditView.tsx @@ -0,0 +1,61 @@ +import * as React from 'react'; +import Helmet from 'react-helmet'; +import { Link } from 'react-router-dom'; +import { translate, TranslateFunction } from '@restapp/i18n-client-react'; +import { PageLayout } from '@restapp/look-client-react'; + +import UserForm from './UserForm'; +import settings from '../../../../settings'; +import { User, ResetPasswordSubmitProps } from '..'; + +interface FormValues extends User, ResetPasswordSubmitProps {} + +interface UserEditViewProps { + loading: boolean; + user: User; + currentUser: User; + t: TranslateFunction; + onSubmit: (values: FormValues) => Promise; +} + +const UserEditView: React.FunctionComponent = ({ loading, user, t, currentUser, onSubmit }) => { + const isNotSelf = !user || (user && user.id !== currentUser.id); + + const renderMetaData = () => ( + + ); + + return ( + + {renderMetaData()} + {loading && !user ? ( +
{t('userEdit.loadMsg')}
+ ) : ( + <> + + Back + +

+ {t('userEdit.form.titleEdit')} {t('userEdit.form.title')} +

+ + + )} +
+ ); +}; + +export default translate('user')(UserEditView); diff --git a/modules/user/client-react/components/UserForm.tsx b/modules/user/client-react/components/UserForm.tsx new file mode 100644 index 0000000..d713144 --- /dev/null +++ b/modules/user/client-react/components/UserForm.tsx @@ -0,0 +1,165 @@ +import * as React from 'react'; +import { withFormik, FormikProps } from 'formik'; +import { isEmpty } from 'lodash'; +import { FieldAdapter as Field } from '@restapp/forms-client-react'; +import { translate } from '@restapp/i18n-client-react'; +import { email as emailRule, minLength, required, match, validate } from '@restapp/validation-common-react'; +import { Form, RenderField, RenderSelect, RenderCheckBox, Option, Button, Alert } from '@restapp/look-client-react'; + +import settings from '../../../../settings'; + +import { CommonProps, FormProps, User, UserRole, ResetPasswordSubmitProps } from '..'; + +interface FormikFormProps extends FormProps { + initialValues: FormValues; +} + +interface UserFormProps extends CommonProps, FormikProps { + handleSubmit: () => void; + handleChange: () => void; + setFieldValue: (name: string, value: any) => void; + onSubmit: (values: FormValues) => Promise; + setTouched: () => void; + isValid: boolean; + shouldDisplayRole: boolean; + shouldDisplayActive: boolean; + values: any; + errors: any; + initialValues: FormValues; +} + +interface FormValues extends User, ResetPasswordSubmitProps {} + +const userFormSchema = { + username: [required, minLength(3)], + email: [required, emailRule] +}; + +const createUserFormSchema = { + ...userFormSchema, + password: [required, minLength(settings.auth.password.minLength)], + passwordConfirmation: [required, match('password'), minLength(settings.auth.password.minLength)] +}; + +const updateUserFormSchema = { + ...userFormSchema, + password: minLength(settings.auth.password.minLength), + passwordConfirmation: [match('password'), minLength(settings.auth.password.minLength)] +}; + +const UserForm: React.FunctionComponent = ({ + values, + handleSubmit, + errors, + setFieldValue, + t, + shouldDisplayRole, + shouldDisplayActive +}) => { + const { username, email, role, isActive, profile, password, passwordConfirmation } = values; + + return ( +
+ + + {shouldDisplayRole && ( + + + + + )} + {shouldDisplayActive && ( + + )} + setFieldValue('profile', { ...profile, firstName: value })} + /> + setFieldValue('profile', { ...profile, lastName: value })} + /> + + + {errors && errors.errorMsg && {errors.errorMsg}} + + + ); +}; + +const UserFormWithFormik = withFormik({ + mapPropsToValues: values => { + const { username, email, role, isActive, profile } = values.initialValues; + return { + username, + email, + role: role || UserRole.user, + isActive, + password: '', + passwordConfirmation: '', + profile: { + firstName: profile && profile.firstName, + lastName: profile && profile.lastName + }, + auth: { + ...values.initialValues.auth + } + }; + }, + async handleSubmit(values, { setErrors, props: { onSubmit } }) { + await onSubmit(values).catch((e: any) => { + if (e && e.errors) { + setErrors(e.errors); + } else { + throw e; + } + }); + }, + displayName: 'SignUpForm ', // helps with React DevTools + validate: (values, props) => { + const schema: any = isEmpty(props.initialValues) ? createUserFormSchema : updateUserFormSchema; + return validate(values, schema); + } +}); + +export default translate('user')(UserFormWithFormik(UserForm)); diff --git a/modules/user/client-react/components/UsersFilterView.tsx b/modules/user/client-react/components/UsersFilterView.tsx new file mode 100644 index 0000000..b90b335 --- /dev/null +++ b/modules/user/client-react/components/UsersFilterView.tsx @@ -0,0 +1,57 @@ +import * as React from 'react'; +import { DebounceInput } from 'react-debounce-input'; +import { translate, TranslateFunction } from '@restapp/i18n-client-react'; +import { Form, FormItem, Select, Option, Label, Input } from '@restapp/look-client-react'; +import { Filter } from '..'; + +export interface UsersFilterViewProps { + filter: Filter; + onSearchTextChange: (value: string) => void; + onRoleChange: (value: string) => void; + onIsActiveChange: (isActive: boolean) => void; + t?: TranslateFunction; +} + +const UsersFilterView: React.FunctionComponent = ({ + filter: { searchText, role, isActive }, + onSearchTextChange, + onRoleChange, + onIsActiveChange, + t +}) => ( +
+ + onSearchTextChange(e.target.value)} + /> + +   + + + +   + + + +
+); + +export default translate('user')(UsersFilterView); diff --git a/modules/user/client-react/components/UsersListView.tsx b/modules/user/client-react/components/UsersListView.tsx new file mode 100644 index 0000000..3a28044 --- /dev/null +++ b/modules/user/client-react/components/UsersListView.tsx @@ -0,0 +1,131 @@ +import * as React from 'react'; +import { Link } from 'react-router-dom'; +import { translate } from '@restapp/i18n-client-react'; +import { Table, Button } from '@restapp/look-client-react'; +import { User } from '..'; +import { OrderBy, CommonProps } from '..'; + +export interface UsersViewProps extends CommonProps { + loading: boolean; + users?: User[]; + orderBy?: OrderBy; + onOrderBy: (orderBy: OrderBy) => void; + deleteUser: (id: number | string) => Promise; +} + +const UsersView = ({ deleteUser, orderBy, onOrderBy, loading, users, t, ...rest }: UsersViewProps) => { + const [errors, setErrors] = React.useState([]); + + const handleDeleteUser = async (id: number | string) => { + const result = await deleteUser(id); + if (result && result.errors) { + setErrors(result.errors); + } else { + setErrors([]); + } + }; + + const renderOrderByArrow = (name: string) => { + if (orderBy && orderBy.column === name) { + if (orderBy.order === 'desc') { + return ; + } else { + return ; + } + } else { + return ; + } + }; + + const handleOrderBy = (e: React.MouseEvent, name: string) => { + e.preventDefault(); + + let order = 'asc'; + if (orderBy && orderBy.column === name) { + if (orderBy.order === 'asc') { + order = 'desc'; + } else if (orderBy.order === 'desc') { + return onOrderBy({ + column: '', + order: '' + }); + } + } + + return onOrderBy({ column: name, order }); + }; + + const columns = [ + { + title: ( + handleOrderBy(e, 'username')} href="#"> + {t('users.column.name')} {renderOrderByArrow('username')} + + ), + dataIndex: 'username', + key: 'username', + render: (text: string, record: User) => ( + + {text} + + ) + }, + { + title: ( + handleOrderBy(e, 'email')} href="#"> + {t('users.column.email')} {renderOrderByArrow('email')} + + ), + dataIndex: 'email', + key: 'email' + }, + { + title: ( + handleOrderBy(e, 'role')} href="#"> + {t('users.column.role')} {renderOrderByArrow('role')} + + ), + dataIndex: 'role', + key: 'role' + }, + { + title: ( + handleOrderBy(e, 'isActive')} href="#"> + {t('users.column.active')} {renderOrderByArrow('isActive')} + + ), + dataIndex: 'isActive', + key: 'isActive', + render: (text: string) => text.toString() + }, + { + title: t('users.column.actions'), + key: 'actions', + render: (_text: string, record: User) => ( + + ) + } + ]; + + return ( + <> + {loading && !users ? ( +
{t('users.loadMsg')}
+ ) : ( + <> + {errors && + errors.map(error => ( +
+ {error.message} +
+ ))} + + + )} + + ); +}; + +export default translate('user')(UsersView); diff --git a/modules/user/client-react/containers/Profile.tsx b/modules/user/client-react/containers/Profile.tsx new file mode 100644 index 0000000..09e4ed8 --- /dev/null +++ b/modules/user/client-react/containers/Profile.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import ProfileView from '../components/ProfileView'; +import { User } from '..'; + +interface ProfileProps { + currentUser: User; + currentUserLoading: boolean; + error: any; +} + +const Profile: React.FunctionComponent = props => { + return ; +}; + +export default connect(({ user: { loading, currentUser } }: any) => { + return { + currentUser, + currentUserLoading: loading + }; +})(Profile); diff --git a/modules/user/client-react/containers/UserAdd.tsx b/modules/user/client-react/containers/UserAdd.tsx new file mode 100644 index 0000000..7718f8e --- /dev/null +++ b/modules/user/client-react/containers/UserAdd.tsx @@ -0,0 +1,62 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { compose } from 'redux'; +import { pick } from 'lodash'; +import { translate } from '@restapp/i18n-client-react'; +import { FormError } from '@restapp/forms-client-react'; + +import UserAddView from '../components/UserAddView'; +import settings from '../../../../settings'; +import UserFormatter from '../helpers/UserFormatter'; +import { CommonProps as RNCommonProps } from '../index.native'; +import { User, CommonProps } from '..'; +import { ADD_USER } from '../actions'; + +interface UserAddProps extends CommonProps, RNCommonProps { + addUser: (values: User) => void; +} + +const UserAdd: React.FunctionComponent = props => { + const { addUser, t, history, navigation } = props; + + const onSubmit = async (values: User) => { + let userValues = pick(values, ['username', 'email', 'role', 'isActive', 'password']) as User; + + userValues.profile = pick(values.profile, ['firstName', 'lastName']); + + userValues = UserFormatter.trimExtraSpaces(userValues) as User; + + if (settings.auth.certificate.enabled) { + userValues.auth = { certificate: pick(values.auth.certificate, 'serial') }; + } + + try { + await addUser(userValues); + } catch (e) { + throw new FormError(t('userAdd.errorMsg'), e); + } + + if (history) { + return history.push('/users/'); + } + if (navigation) { + return navigation.goBack(); + } + }; + + return ; +}; + +export default compose( + translate('user'), + connect( + _state => ({}), + dispatch => ({ + addUser: (values: User) => + dispatch({ + type: null, + request: () => ADD_USER(values) + }) + }) + )(UserAdd) +); diff --git a/modules/user/client-react/containers/UserEdit.tsx b/modules/user/client-react/containers/UserEdit.tsx new file mode 100644 index 0000000..aaffcc0 --- /dev/null +++ b/modules/user/client-react/containers/UserEdit.tsx @@ -0,0 +1,69 @@ +import * as React from 'react'; +import { connect } from 'react-redux'; +import { compose } from 'redux'; +import { pick } from 'lodash'; +import { translate } from '@restapp/i18n-client-react'; +import { FormError } from '@restapp/forms-client-react'; +import UserEditView from '../components/UserEditView'; + +import UserFormatter from '../helpers/UserFormatter'; +import { User, CommonProps } from '..'; +import { CommonProps as RNCommonProps } from '../index.native'; +import { UserModuleState } from '../reducers'; +import { USER, EDIT_USER } from '../actions'; + +interface UserEditProps extends CommonProps, RNCommonProps { + user: User; + editUser: (value: User) => void; + location: any; + match: any; + getUser?: (id: number) => void; +} + +class UserEdit extends React.Component { + public componentDidMount() { + let id = 0; + if (this.props.match) { + id = this.props.match.params.id; + } else if (this.props.navigation) { + id = this.props.navigation.state.params.id; + } + this.props.getUser(Number(id)); + } + public onSubmit = async (values: User) => { + const { user, editUser, t, navigation, history } = this.props; + let userValues = pick(values, ['username', 'email', 'role', 'isActive', 'password']) as User; + + userValues.profile = pick(values.profile, ['firstName', 'lastName']); + + userValues = UserFormatter.trimExtraSpaces(userValues) as User; + + try { + await editUser({ id: user.id, ...userValues }); + } catch (e) { + throw new FormError(t('userEdit.errorMsg'), e); + } + + if (history) { + return history.goBack(); + } + + if (navigation) { + return navigation.goBack(); + } + }; + + public render() { + return ; + } +} + +export default compose( + translate('user'), + connect( + ({ user }: UserModuleState) => ({ + user + }), + { getUser: USER, editUser: EDIT_USER } + )(UserEdit) +); diff --git a/modules/user/client-react/containers/UserOperations.tsx b/modules/user/client-react/containers/UserOperations.tsx new file mode 100644 index 0000000..c61951e --- /dev/null +++ b/modules/user/client-react/containers/UserOperations.tsx @@ -0,0 +1,69 @@ +import * as React from 'react'; +import { connect } from 'react-redux'; +import { USERS, DELETE_USER } from '../actions'; +import { UserRole, OrderBy } from '..'; + +const withUsers = (Component: React.ComponentType) => { + class WithUsers extends React.Component { + public state = { + ready: false + }; + + public async componentDidMount() { + const { getUsers, orderBy, filter } = this.props; + if (!this.state.ready) { + await getUsers(orderBy, filter); + } + this.setState({ ready: true }); + } + + public render() { + const { getUsers, ...props } = this.props; + return ; + } + } + return connect( + ({ user: { loading, users } }: any) => ({ + loading, + users + }), + { getUsers: USERS } + )(WithUsers); +}; + +const withUsersDeleting = (Component: React.ComponentType) => + connect( + null, + { deleteUser: DELETE_USER } + )(Component); + +const withSortAndFilter = (Component: React.ComponentType) => { + const WithFilterUpdating: React.ComponentType = ({ orderBy, filter, sortAndFilter, ...props }) => { + const onOrderBy = (_orderBy: OrderBy) => sortAndFilter(_orderBy, filter, 'SET_ORDER_BY'); + const onSearchTextChange = (searchText: string) => sortAndFilter(orderBy, { ...filter, searchText }, 'SET_FILTER'); + const onRoleChange = (role: UserRole) => sortAndFilter(orderBy, { ...filter, role }, 'SET_FILTER'); + const onIsActiveChange = (isActive: boolean) => sortAndFilter(orderBy, { ...filter, isActive }, 'SET_FILTER'); + + return ( + + ); + }; + + return connect( + ({ user: { orderBy, filter } }: any) => ({ + orderBy, + filter + }), + { sortAndFilter: USERS } + )(WithFilterUpdating); +}; + +export { withUsers, withUsersDeleting, withSortAndFilter }; diff --git a/modules/user/client-react/containers/Users.tsx b/modules/user/client-react/containers/Users.tsx new file mode 100644 index 0000000..59469f9 --- /dev/null +++ b/modules/user/client-react/containers/Users.tsx @@ -0,0 +1,49 @@ +import * as React from 'react'; +import Helmet from 'react-helmet'; +import { compose } from 'redux'; +import { Link } from 'react-router-dom'; +import { translate, TranslateFunction } from '@restapp/i18n-client-react'; +import { Button, PageLayout } from '@restapp/look-client-react'; +import settings from '../../../../settings'; +import UsersFilterView, { UsersFilterViewProps } from '../components/UsersFilterView'; +import UsersListView, { UsersViewProps } from '../components/UsersListView'; +import { withSortAndFilter, withUsers, withUsersDeleting } from './UserOperations'; + +interface UsersProps extends UsersViewProps, UsersFilterViewProps {} + +class Users extends React.Component { + public renderMetaData = (t: TranslateFunction) => ( + + ); + public render() { + const { t } = this.props; + + return ( + + {this.renderMetaData(t)} +

{t('users.list.title')}

+ + + +
+ +
+ +
+ ); + } +} + +export default compose( + withUsersDeleting, + withSortAndFilter, + withUsers +)(translate('user')(Users)); diff --git a/modules/user/client-react/index.tsx b/modules/user/client-react/index.tsx index bb9976d..5142a51 100644 --- a/modules/user/client-react/index.tsx +++ b/modules/user/client-react/index.tsx @@ -9,11 +9,15 @@ import { FormikErrors } from 'formik'; import resources from './locales'; import DataRootComponent from './containers/DataRootComponent'; -import Register from './containers/Register'; import Login from './containers/Login'; +import Register from './containers/Register'; +import Users from './containers/Users'; +import UserEdit from './containers/UserEdit'; +import UserAdd from './containers/UserAdd'; +import Profile from './containers/Profile'; import reducers from './reducers'; -import { AuthRoute, IfLoggedIn, IfNotLoggedIn, withLogout, WithLogoutProps } from './containers/Auth'; +import { AuthRoute, IfLoggedIn, IfNotLoggedIn, withLoadedUser, withLogout, WithLogoutProps } from './containers/Auth'; export enum UserRole { admin = 'admin', @@ -88,6 +92,10 @@ export interface Filter { isActive: boolean; } +const ProfileName = withLoadedUser(({ currentUser }) => ( + <>{currentUser ? currentUser.fullName || currentUser.username : null} +)); + const LogoutLink = withRouter(withLogout(({ logout, history }: WithLogoutProps) => ( ( + + {t('navLink.users')} + +)); + const NavLinkLoginWithI18n = translate('user')(({ t }: any) => ( {t('navLink.signIn')} @@ -114,10 +128,28 @@ const NavLinkLoginWithI18n = translate('user')(({ t }: any) => ( export default new ClientModule({ route: [ - , - + , + , + , + , + , + + ], + navItem: [ + + + + + ], navItemRight: [ + + + + + + + , diff --git a/modules/user/client-react/reducers/index.ts b/modules/user/client-react/reducers/index.ts index 7a015d0..61737b2 100644 --- a/modules/user/client-react/reducers/index.ts +++ b/modules/user/client-react/reducers/index.ts @@ -1,13 +1,15 @@ import { User, OrderBy, Filter } from '..'; export enum ActionType { + null = 'null', SET_CURRENT_USER = 'SET_CURRENT_USER', CLEAR_CURRENT_USER = 'CLEAR_CURRENT_USER', SET_LOADING = 'SET_LOADING', SET_USER = 'SET_USER', SET_USERS = 'SET_USERS', SET_ORDER_BY = 'SET_ORDER_BY', - SET_FILTER = 'SET_FILTER' + SET_FILTER = 'SET_FILTER', + DELETE_USER = 'DELETE_USER' } export interface UserModuleState { @@ -32,7 +34,7 @@ const defaultState: UserModuleState = { user: null, users: [], orderBy: { column: '', order: '' }, - filter: { searchText: '', role: null, isActive: true } + filter: { searchText: '', role: '', isActive: true } }; export default function(state = defaultState, action: UserModuleActionProps) { @@ -82,6 +84,13 @@ export default function(state = defaultState, action: UserModuleActionProps) { orderBy: action.payload && action.payload.orderBy }; + case ActionType.DELETE_USER: + const users = state.users.filter(user => user.id !== action.payload.id); + return { + ...state, + users + }; + default: return state; } From d9b694ebc58b6e90cfaafc970745402eae92c1e4 Mon Sep 17 00:00:00 2001 From: Alexandr Belokur Date: Mon, 13 May 2019 16:12:13 +0300 Subject: [PATCH 046/104] Remove extra property in user conrtoller --- modules/user/server-ts/controllers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/user/server-ts/controllers.ts b/modules/user/server-ts/controllers.ts index c139a1e..ca243e2 100644 --- a/modules/user/server-ts/controllers.ts +++ b/modules/user/server-ts/controllers.ts @@ -19,7 +19,7 @@ const { export const user = async ({ params: { id }, user: identity, t }: any, res: any) => { if (+identity.id === +id || identity.role === 'admin') { try { - res.json({ user: await userDAO.getUser(id) }); + res.json(await userDAO.getUser(id)); } catch (e) { res.status(500).json({ errors: e }); } From be5a69e14950c6b8dba9eca5c04cd0686307ec9f Mon Sep 17 00:00:00 2001 From: Ivan Isakov Date: Mon, 13 May 2019 18:11:17 +0300 Subject: [PATCH 047/104] Fix add and edit user --- modules/forms/client-react/FieldAdapter.jsx | 1 + modules/user/client-react/actions/addUser.ts | 2 +- modules/user/client-react/actions/editUser.ts | 3 +- .../client-react/components/ProfileView.tsx | 4 +-- .../user/client-react/components/UserForm.tsx | 23 +++++-------- modules/user/client-react/containers/Auth.tsx | 2 +- .../user/client-react/containers/UserAdd.tsx | 33 +++++-------------- .../user/client-react/containers/UserEdit.tsx | 30 ++++++++--------- modules/user/client-react/index.tsx | 17 +++++++--- 9 files changed, 52 insertions(+), 63 deletions(-) diff --git a/modules/forms/client-react/FieldAdapter.jsx b/modules/forms/client-react/FieldAdapter.jsx index fe55444..c513a23 100644 --- a/modules/forms/client-react/FieldAdapter.jsx +++ b/modules/forms/client-react/FieldAdapter.jsx @@ -46,6 +46,7 @@ class FieldAdapter extends Component { }; onChangeText = value => { + console.log('TCL: FieldAdapter -> value', value); const { formik, onChangeText, onChange, name } = this.props; if (onChange && !onChangeText) { onChange(value); diff --git a/modules/user/client-react/actions/addUser.ts b/modules/user/client-react/actions/addUser.ts index a1e3082..fb9dff2 100644 --- a/modules/user/client-react/actions/addUser.ts +++ b/modules/user/client-react/actions/addUser.ts @@ -4,7 +4,7 @@ import { User } from '..'; export default function ADD_USER(user: User) { return { - types: [null, ActionType.SET_CURRENT_USER, ActionType.CLEAR_CURRENT_USER], + types: [null, null, null] as ActionType[], callAPI: () => axios.post(`${__API_URL__}/addUser`, { ...user }) }; } diff --git a/modules/user/client-react/actions/editUser.ts b/modules/user/client-react/actions/editUser.ts index 17300ff..3a12be0 100644 --- a/modules/user/client-react/actions/editUser.ts +++ b/modules/user/client-react/actions/editUser.ts @@ -1,9 +1,10 @@ import axios from 'axios'; +import { ActionType } from '../reducers'; import { User } from '..'; export default function EDIT_USER(user: User) { return { - types: [null, null, null] as any, + types: [null, null, null] as ActionType[], callAPI: () => axios.post(`${__API_URL__}/editUser`, { ...user }) }; } diff --git a/modules/user/client-react/components/ProfileView.tsx b/modules/user/client-react/components/ProfileView.tsx index 04dab1f..89b1a21 100644 --- a/modules/user/client-react/components/ProfileView.tsx +++ b/modules/user/client-react/components/ProfileView.tsx @@ -54,10 +54,10 @@ const ProfileView: React.FunctionComponent = ({ currentUserLoa {t('profile.card.group.role')}: {currentUser.role} - {currentUser.profile && currentUser.profile.fullName && ( + {currentUser && currentUser.fullName && ( {t('profile.card.group.full')}: - {currentUser.profile.fullName} + {currentUser.fullName} )} diff --git a/modules/user/client-react/components/UserForm.tsx b/modules/user/client-react/components/UserForm.tsx index d713144..b6cd7fa 100644 --- a/modules/user/client-react/components/UserForm.tsx +++ b/modules/user/client-react/components/UserForm.tsx @@ -52,11 +52,12 @@ const UserForm: React.FunctionComponent = ({ handleSubmit, errors, setFieldValue, + t, shouldDisplayRole, shouldDisplayActive }) => { - const { username, email, role, isActive, profile, password, passwordConfirmation } = values; + const { username, email, role, isActive, lastName, firstName, password, passwordConfirmation } = values; return (
@@ -86,7 +87,7 @@ const UserForm: React.FunctionComponent = ({ component={RenderCheckBox} type="checkbox" label={t('userEdit.form.field.active')} - checked={isActive} + checked={!!isActive} /> )} = ({ component={RenderField} type="text" label={t('userEdit.form.field.firstName')} - value={profile.firstName} - onChange={(value: string) => setFieldValue('profile', { ...profile, firstName: value })} + value={firstName} /> setFieldValue('profile', { ...profile, lastName: value })} + value={lastName} /> = ({ const UserFormWithFormik = withFormik({ mapPropsToValues: values => { - const { username, email, role, isActive, profile } = values.initialValues; + const { username, email, role, isActive, firstName, lastName, ...rest } = values.initialValues; return { username, email, @@ -137,13 +136,9 @@ const UserFormWithFormik = withFormik({ isActive, password: '', passwordConfirmation: '', - profile: { - firstName: profile && profile.firstName, - lastName: profile && profile.lastName - }, - auth: { - ...values.initialValues.auth - } + firstName, + lastName, + ...rest }; }, async handleSubmit(values, { setErrors, props: { onSubmit } }) { diff --git a/modules/user/client-react/containers/Auth.tsx b/modules/user/client-react/containers/Auth.tsx index 08f7122..5879d16 100644 --- a/modules/user/client-react/containers/Auth.tsx +++ b/modules/user/client-react/containers/Auth.tsx @@ -12,7 +12,7 @@ interface AuthRouteProps extends WithUserProps { } const AuthRoute: React.ComponentType = withLoadedUser( - ({ currentUser, role, redirect = '/register', redirectOnLoggedIn, component: Component, ...rest }) => { + ({ currentUser, role, redirect = '/login', redirectOnLoggedIn, component: Component, ...rest }) => { const RenderComponent: React.FunctionComponent = props => { // The users is not logged in if (redirectOnLoggedIn && currentUser) { diff --git a/modules/user/client-react/containers/UserAdd.tsx b/modules/user/client-react/containers/UserAdd.tsx index 7718f8e..f05ffc4 100644 --- a/modules/user/client-react/containers/UserAdd.tsx +++ b/modules/user/client-react/containers/UserAdd.tsx @@ -1,39 +1,31 @@ import React from 'react'; import { connect } from 'react-redux'; -import { compose } from 'redux'; import { pick } from 'lodash'; import { translate } from '@restapp/i18n-client-react'; import { FormError } from '@restapp/forms-client-react'; import UserAddView from '../components/UserAddView'; -import settings from '../../../../settings'; import UserFormatter from '../helpers/UserFormatter'; import { CommonProps as RNCommonProps } from '../index.native'; import { User, CommonProps } from '..'; import { ADD_USER } from '../actions'; interface UserAddProps extends CommonProps, RNCommonProps { - addUser: (values: User) => void; + addUser: (values: User) => any; } const UserAdd: React.FunctionComponent = props => { const { addUser, t, history, navigation } = props; const onSubmit = async (values: User) => { - let userValues = pick(values, ['username', 'email', 'role', 'isActive', 'password']) as User; + let userValues = pick(values, ['username', 'email', 'role', 'isActive', 'password', 'firstName', 'lastName']); - userValues.profile = pick(values.profile, ['firstName', 'lastName']); + userValues = UserFormatter.trimExtraSpaces(userValues); - userValues = UserFormatter.trimExtraSpaces(userValues) as User; + const data = await addUser(userValues as User); - if (settings.auth.certificate.enabled) { - userValues.auth = { certificate: pick(values.auth.certificate, 'serial') }; - } - - try { - await addUser(userValues); - } catch (e) { - throw new FormError(t('userAdd.errorMsg'), e); + if (data && data.errors) { + throw new FormError(t('userAdd.errorMsg'), data); } if (history) { @@ -47,16 +39,9 @@ const UserAdd: React.FunctionComponent = props => { return ; }; -export default compose( - translate('user'), +export default translate('user')( connect( - _state => ({}), - dispatch => ({ - addUser: (values: User) => - dispatch({ - type: null, - request: () => ADD_USER(values) - }) - }) + null, + { addUser: ADD_USER } )(UserAdd) ); diff --git a/modules/user/client-react/containers/UserEdit.tsx b/modules/user/client-react/containers/UserEdit.tsx index aaffcc0..f507885 100644 --- a/modules/user/client-react/containers/UserEdit.tsx +++ b/modules/user/client-react/containers/UserEdit.tsx @@ -1,6 +1,5 @@ import * as React from 'react'; import { connect } from 'react-redux'; -import { compose } from 'redux'; import { pick } from 'lodash'; import { translate } from '@restapp/i18n-client-react'; import { FormError } from '@restapp/forms-client-react'; @@ -9,39 +8,39 @@ import UserEditView from '../components/UserEditView'; import UserFormatter from '../helpers/UserFormatter'; import { User, CommonProps } from '..'; import { CommonProps as RNCommonProps } from '../index.native'; -import { UserModuleState } from '../reducers'; import { USER, EDIT_USER } from '../actions'; interface UserEditProps extends CommonProps, RNCommonProps { user: User; - editUser: (value: User) => void; + editUser: (value: User) => any; location: any; match: any; getUser?: (id: number) => void; } class UserEdit extends React.Component { - public componentDidMount() { + public state = { ready: false }; + public async componentDidMount() { let id = 0; if (this.props.match) { id = this.props.match.params.id; } else if (this.props.navigation) { id = this.props.navigation.state.params.id; } - this.props.getUser(Number(id)); + await this.props.getUser(Number(id)); + this.setState({ ready: true }); } public onSubmit = async (values: User) => { const { user, editUser, t, navigation, history } = this.props; - let userValues = pick(values, ['username', 'email', 'role', 'isActive', 'password']) as User; - userValues.profile = pick(values.profile, ['firstName', 'lastName']); + let userValues = pick(values, ['username', 'email', 'role', 'isActive', 'password', 'firstName', 'lastName']); - userValues = UserFormatter.trimExtraSpaces(userValues) as User; + userValues = UserFormatter.trimExtraSpaces(userValues); - try { - await editUser({ id: user.id, ...userValues }); - } catch (e) { - throw new FormError(t('userEdit.errorMsg'), e); + const data = await editUser({ id: user.id, ...userValues } as any); + + if (data && data.errors) { + throw new FormError(t('userEdit.errorMsg'), data); } if (history) { @@ -54,14 +53,13 @@ class UserEdit extends React.Component { }; public render() { - return ; + return this.state.ready ? : null; } } -export default compose( - translate('user'), +export default translate('user')( connect( - ({ user }: UserModuleState) => ({ + ({ user: { user } }: any) => ({ user }), { getUser: USER, editUser: EDIT_USER } diff --git a/modules/user/client-react/index.tsx b/modules/user/client-react/index.tsx index 5142a51..0f05a53 100644 --- a/modules/user/client-react/index.tsx +++ b/modules/user/client-react/index.tsx @@ -24,20 +24,29 @@ export enum UserRole { user = 'user' } -export interface UserProfile { +interface Auth { + lnDisplayName: string; + lnId: string; + googleDisplayName: string; + googleId: string; + ghDisplayName: string; + ghId: string; + fbId: string; + fbDisplayName: string; +} + +interface UserProfile { fullName?: string; firstName?: string; lastName?: string; } -export interface User { +export interface User extends UserProfile, Auth { id?: number | string; username: string; role: UserRole; isActive: boolean; email: string; - profile?: UserProfile; - auth?: any; } export interface LoginSubmitProps { From 454c1294e0ffba3431b71936c5ff7e11822d5ef8 Mon Sep 17 00:00:00 2001 From: Ivan Isakov Date: Mon, 13 May 2019 18:31:55 +0300 Subject: [PATCH 048/104] Fix react-native user interface --- modules/user/client-react/index.native.tsx | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/modules/user/client-react/index.native.tsx b/modules/user/client-react/index.native.tsx index b02615b..17a5369 100644 --- a/modules/user/client-react/index.native.tsx +++ b/modules/user/client-react/index.native.tsx @@ -24,20 +24,29 @@ export enum UserRole { user = 'user' } -export interface UserProfile { +interface Auth { + lnDisplayName: string; + lnId: string; + googleDisplayName: string; + googleId: string; + ghDisplayName: string; + ghId: string; + fbId: string; + fbDisplayName: string; +} + +interface UserProfile { fullName?: string; firstName?: string; lastName?: string; } -export interface User { +export interface User extends UserProfile, Auth { id?: number | string; username: string; role: UserRole; isActive: boolean; email: string; - profile?: UserProfile; - auth?: any; } export interface NavigationOptionsProps { From 9591392f69f69764977cc729a27db5c39537dd46 Mon Sep 17 00:00:00 2001 From: Ivan Isakov Date: Tue, 14 May 2019 11:26:47 +0300 Subject: [PATCH 049/104] Add try catch for form validations --- modules/authentication/client-react/access/jwt/index.ts | 2 +- modules/user/client-react/containers/Login.native.tsx | 6 ++++-- modules/user/client-react/containers/Login.tsx | 7 ++++--- modules/user/client-react/containers/Register.native.tsx | 7 ++++--- modules/user/client-react/containers/Register.tsx | 7 ++++--- modules/user/client-react/containers/UserAdd.tsx | 7 ++++--- modules/user/client-react/containers/UserEdit.tsx | 7 ++++--- 7 files changed, 25 insertions(+), 18 deletions(-) diff --git a/modules/authentication/client-react/access/jwt/index.ts b/modules/authentication/client-react/access/jwt/index.ts index d09af1e..72e1d21 100644 --- a/modules/authentication/client-react/access/jwt/index.ts +++ b/modules/authentication/client-react/access/jwt/index.ts @@ -54,7 +54,7 @@ const requestMiddleware: Middleware = ({ dispatch }) => next => action => { ...rest, payload: data }); - return data; + throw e; } }; diff --git a/modules/user/client-react/containers/Login.native.tsx b/modules/user/client-react/containers/Login.native.tsx index b4d0d44..0e2bc60 100644 --- a/modules/user/client-react/containers/Login.native.tsx +++ b/modules/user/client-react/containers/Login.native.tsx @@ -14,9 +14,11 @@ export interface LoginProps extends CommonProps { class Login extends React.Component { public onSubmit = async (values: LoginSubmitProps) => { const { t, login } = this.props; - const data = await login(values); - if (data && data.errors) { + try { + await login(values); + } catch (e) { + const data = e.response && e.response.data; throw new FormError(t('reg.errorMsg'), data); } diff --git a/modules/user/client-react/containers/Login.tsx b/modules/user/client-react/containers/Login.tsx index fb45fa9..dbfc77e 100644 --- a/modules/user/client-react/containers/Login.tsx +++ b/modules/user/client-react/containers/Login.tsx @@ -35,9 +35,10 @@ const Login: React.FunctionComponent = props => { }; const onSubmit = async (values: LoginSubmitProps) => { - const data = await login(values); - - if (data && data.errors) { + try { + await login(values); + } catch (e) { + const data = e.response && e.response.data; throw new FormError(t('reg.errorMsg'), data); } diff --git a/modules/user/client-react/containers/Register.native.tsx b/modules/user/client-react/containers/Register.native.tsx index 3f99a63..936e58b 100644 --- a/modules/user/client-react/containers/Register.native.tsx +++ b/modules/user/client-react/containers/Register.native.tsx @@ -24,9 +24,10 @@ class Register extends React.Component { public onSubmit = async (values: RegisterSubmitProps) => { const { t, register, navigation } = this.props; - const data = await register(values); - - if (data.errors) { + try { + await await register(values); + } catch (e) { + const data = e.response && e.response.data; throw new FormError(t('reg.errorMsg'), data); } diff --git a/modules/user/client-react/containers/Register.tsx b/modules/user/client-react/containers/Register.tsx index 4af4b22..5e10ed6 100644 --- a/modules/user/client-react/containers/Register.tsx +++ b/modules/user/client-react/containers/Register.tsx @@ -19,9 +19,10 @@ const Register: React.FunctionComponent = props => { const [isRegistered, setIsRegistered] = React.useState(false); const onSubmit = async (values: RegisterSubmitProps) => { - const data = await register(values); - - if (data.errors) { + try { + await await register(values); + } catch (e) { + const data = e.response && e.response.data; throw new FormError(t('reg.errorMsg'), data); } diff --git a/modules/user/client-react/containers/UserAdd.tsx b/modules/user/client-react/containers/UserAdd.tsx index f05ffc4..7c571f7 100644 --- a/modules/user/client-react/containers/UserAdd.tsx +++ b/modules/user/client-react/containers/UserAdd.tsx @@ -22,9 +22,10 @@ const UserAdd: React.FunctionComponent = props => { userValues = UserFormatter.trimExtraSpaces(userValues); - const data = await addUser(userValues as User); - - if (data && data.errors) { + try { + await addUser(userValues as User); + } catch (e) { + const data = e.response && e.response.data; throw new FormError(t('userAdd.errorMsg'), data); } diff --git a/modules/user/client-react/containers/UserEdit.tsx b/modules/user/client-react/containers/UserEdit.tsx index f507885..95e04d2 100644 --- a/modules/user/client-react/containers/UserEdit.tsx +++ b/modules/user/client-react/containers/UserEdit.tsx @@ -37,9 +37,10 @@ class UserEdit extends React.Component { userValues = UserFormatter.trimExtraSpaces(userValues); - const data = await editUser({ id: user.id, ...userValues } as any); - - if (data && data.errors) { + try { + await editUser({ id: user.id, ...userValues } as any); + } catch (e) { + const data = e.response && e.response.data; throw new FormError(t('userEdit.errorMsg'), data); } From 2cbb16733979c124d38fe24785ac462a2cb91059 Mon Sep 17 00:00:00 2001 From: Alexandr Belokur Date: Tue, 14 May 2019 11:39:10 +0300 Subject: [PATCH 050/104] Add check for accessMiddleware --- modules/module/server-ts/ServerModule.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/module/server-ts/ServerModule.ts b/modules/module/server-ts/ServerModule.ts index 056aec7..4592690 100644 --- a/modules/module/server-ts/ServerModule.ts +++ b/modules/module/server-ts/ServerModule.ts @@ -103,7 +103,7 @@ class ServerModule extends CommonModule { return (app: Express, modules: ServerModule) => { const handlers = []; - if (isAuthRoute) { + if (isAuthRoute && modules.accessMiddleware) { handlers.push(modules.accessMiddleware); } if (!isEmpty(middleware)) { From 658c71c4b6365aca68a3cbba25619becf902af32 Mon Sep 17 00:00:00 2001 From: Alexandr Belokur Date: Tue, 14 May 2019 12:23:54 +0300 Subject: [PATCH 051/104] Add forgot and reset password conrtollers --- .../user/server-ts/password/controllers.ts | 75 +++++++++++++++++++ modules/user/server-ts/password/index.ts | 16 +++- 2 files changed, 88 insertions(+), 3 deletions(-) diff --git a/modules/user/server-ts/password/controllers.ts b/modules/user/server-ts/password/controllers.ts index 584360f..429c573 100644 --- a/modules/user/server-ts/password/controllers.ts +++ b/modules/user/server-ts/password/controllers.ts @@ -90,3 +90,78 @@ export const register = async ({ body, t }: any, res: any) => { res.json(user); }; + +export const forgotPassword = async ({ body, t }: any, res: any) => { + try { + const localAuth = pick(body, 'email'); + const identity = (await userDAO.getUserByEmail(localAuth.email)) as UserShape; + + if (identity && mailer) { + // async email + jwt.sign( + { email: identity.email, passwordHash: identity.passwordHash }, + secret, + { expiresIn: '1d' }, + (err, emailToken) => { + // encoded token since react router does not match dots in params + const encodedToken = Buffer.from(emailToken).toString('base64'); + const url = `${__WEBSITE_URL__}/reset-password/${encodedToken}`; + mailer.sendMail({ + from: `${app.name} <${process.env.EMAIL_USER}>`, + to: identity.email, + subject: 'Reset Password', + html: emailTemplate.passwordReset(url) + }); + log.info(`Sent link to reset email to: ${identity.email}`); + } + ); + res.send(t('user:auth.password.forgotPassword')); + } + } catch (e) { + res.send(e); + } +}; + +export const resetPassword = async ({ body, t }: any, res: any) => { + const errors: ValidationErrors = {}; + const reset = pick(body, ['password', 'passwordConfirmation', 'token']); + + if (reset.password !== reset.passwordConfirmation) { + errors.password = t('user:auth.password.passwordsIsNotMatch'); + } + + if (reset.password.length < password.minLength) { + errors.password = t('user:auth.password.passwordLength', { length: password.minLength }); + } + + if (!isEmpty(errors)) { + return res.status(422).send({ + message: 'Failed reset password', + ...errors + }); + } + + const token = Buffer.from(reset.token, 'base64').toString(); + const { email, passwordHash } = jwt.verify(token, secret) as UserShape; + const identity = (await userDAO.getUserByEmail(email)) as UserShape; + + if (identity.passwordHash !== passwordHash) { + throw res.status(401).send(t('user:auth.password.invalidToken')); + } + + if (identity) { + await userDAO.updatePassword(identity.id, reset.password); + const url = `${__WEBSITE_URL__}/profile`; + res.send(t('user:auth.password.resestPassword')); + + if (mailer && password.sendPasswordChangesEmail) { + mailer.sendMail({ + from: `${app.name} <${process.env.EMAIL_USER}>`, + to: identity.email, + subject: 'Your Password Has Been Updated', + html: emailTemplate.passwordUpdated(url) + }); + log.info(`Sent password has been updated to: ${identity.email}`); + } + } +}; diff --git a/modules/user/server-ts/password/index.ts b/modules/user/server-ts/password/index.ts index 0a5db44..4c3f2bf 100644 --- a/modules/user/server-ts/password/index.ts +++ b/modules/user/server-ts/password/index.ts @@ -2,7 +2,7 @@ import bcrypt from 'bcryptjs'; import ServerModule, { RestMethod } from '@restapp/module-server-ts'; -import { register, login } from './controllers'; +import { login, register, forgotPassword, resetPassword } from './controllers'; import settings from '../../../../settings'; export const createPasswordHash = (pswd: string) => bcrypt.hash(pswd, 12); @@ -10,6 +10,11 @@ export const createPasswordHash = (pswd: string) => bcrypt.hash(pswd, 12); export default (settings.auth.password.enabled ? new ServerModule({ apiRouteParams: [ + { + method: RestMethod.POST, + route: 'login', + controller: login + }, { method: RestMethod.POST, route: 'register', @@ -17,8 +22,13 @@ export default (settings.auth.password.enabled }, { method: RestMethod.POST, - route: 'login', - controller: login + route: 'forgotPassword', + controller: forgotPassword + }, + { + method: RestMethod.POST, + route: 'resetPassword', + controller: resetPassword } ] }) From 42970ec773355f8f6a77a0b1fcba7223dce68cb5 Mon Sep 17 00:00:00 2001 From: Alexandr Belokur Date: Tue, 14 May 2019 13:25:34 +0300 Subject: [PATCH 052/104] Moved passport loging middleware to auth module --- .../server-ts/access/jwt/index.ts | 32 ++++++++++++++- .../server-ts/access/session/index.ts | 33 ++++++++++++++- modules/user/server-ts/index.ts | 2 +- .../user/server-ts/password/controllers.ts | 40 +++++++------------ 4 files changed, 76 insertions(+), 31 deletions(-) diff --git a/modules/authentication/server-ts/access/jwt/index.ts b/modules/authentication/server-ts/access/jwt/index.ts index 1856e91..7430eb7 100644 --- a/modules/authentication/server-ts/access/jwt/index.ts +++ b/modules/authentication/server-ts/access/jwt/index.ts @@ -29,15 +29,36 @@ const grant = async (identity: any, req: any, passwordHash: string = '') => { }; }; +const loginMiddleware = (req: any, res: any, next: any) => { + passport.authenticate('local', { session: settings.auth.session.enabled }, (err, user, info) => { + if (err || !user) { + return res.status(400).json({ + errors: { + message: info ? info.message : 'Login failed' + } + }); + } + + req.login(user, { session: settings.auth.session.enabled }, async (loginErr: any) => { + if (loginErr) { + res.send(loginErr); + } + const tokens = settings.auth.jwt.enabled ? await grant(user, req, user.passwordHash) : null; + + return res.json({ user, tokens }); + }); + })(req, res, next); +}; + const onAppCreate = ({ appContext }: AccessModule) => { passport.use( new LocalStratery({ usernameField: 'usernameOrEmail' }, async (username: string, password: string, done: any) => { - const { identity, message } = await appContext.user.validateLogin(username, password); + const { user, message } = await appContext.user.validateLogin(username, password); if (message) { return done(null, false, { message }); } - return done(null, identity); + return done(null, user); }) ); @@ -54,11 +75,18 @@ const onAppCreate = ({ appContext }: AccessModule) => { ); }; +const jwtAppContext = { + auth: { + loginMiddleware + } +}; + export default (settings.auth.jwt.enabled ? new AccessModule({ beforeware: [beforeware], onAppCreate: [onAppCreate], grant: [grant], + appContext: jwtAppContext, apiRouteParams: [ { method: RestMethod.POST, diff --git a/modules/authentication/server-ts/access/session/index.ts b/modules/authentication/server-ts/access/session/index.ts index bbeda25..78f438f 100644 --- a/modules/authentication/server-ts/access/session/index.ts +++ b/modules/authentication/server-ts/access/session/index.ts @@ -5,6 +5,7 @@ import passport from 'passport'; import { RestMethod } from '@restapp/module-server-ts'; +import { access } from '../../'; import settings from '../../../../../settings'; import AccessModule from '../AccessModule'; import { logout } from './controllers'; @@ -28,6 +29,27 @@ const beforeware = (app: Express) => { const accessMiddleware = (req: Request, res: Response, next: any) => req.isAuthenticated() ? next() : res.send('unauthorized'); +const loginMiddleware = (req: any, res: any, next: any) => { + passport.authenticate('local', { session: settings.auth.session.enabled }, (err, user, info) => { + if (err || !user) { + return res.status(400).json({ + errors: { + message: info ? info.message : 'Login failed' + } + }); + } + + req.login(user, { session: settings.auth.session.enabled }, async (loginErr: any) => { + if (loginErr) { + res.send(loginErr); + } + const tokens = settings.auth.jwt.enabled ? await access.grantAccess(user, req, user.passwordHash) : null; + + return res.json({ user, tokens }); + }); + })(req, res, next); +}; + const onAppCreate = ({ appContext }: AccessModule) => { passport.serializeUser((identity: { id: number }, cb) => { cb(null, identity.id); @@ -40,21 +62,28 @@ const onAppCreate = ({ appContext }: AccessModule) => { passport.use( new Strategy({ usernameField: 'usernameOrEmail' }, async (username: string, password: string, done: any) => { - const { identity, message } = await appContext.user.validateLogin(username, password); + const { user, message } = await appContext.user.validateLogin(username, password); if (message) { return done(null, false, { message }); } - return done(null, identity); + return done(null, user); }) ); }; +const sessionAppContext = { + auth: { + loginMiddleware + } +}; + export default (settings.auth.session.enabled ? new AccessModule({ beforeware: [beforeware], onAppCreate: [onAppCreate], accessMiddleware, + appContext: sessionAppContext, apiRouteParams: [ { method: RestMethod.POST, diff --git a/modules/user/server-ts/index.ts b/modules/user/server-ts/index.ts index 8f675ed..404b7ab 100644 --- a/modules/user/server-ts/index.ts +++ b/modules/user/server-ts/index.ts @@ -37,7 +37,7 @@ const validateLogin = async (usernameOrEmail: string, pswd: string) => { return { message: i18n.t('user:auth.password.validPassword') }; } - return { identity }; + return { user: identity }; }; const appContext = { diff --git a/modules/user/server-ts/password/controllers.ts b/modules/user/server-ts/password/controllers.ts index 429c573..27405c1 100644 --- a/modules/user/server-ts/password/controllers.ts +++ b/modules/user/server-ts/password/controllers.ts @@ -1,8 +1,6 @@ import { UserShape } from './../sql'; import { pick, isEmpty } from 'lodash'; import jwt from 'jsonwebtoken'; -import passport from 'passport'; -import { access } from '@restapp/authentication-server-ts'; import { log } from '@restapp/core-common'; import { mailer } from '@restapp/mailer-server-ts'; @@ -13,29 +11,19 @@ import emailTemplate from '../emailTemplate'; import { createPasswordHash } from '.'; const { - auth: { session, jwt: jwtSetting, password, secret }, + auth: { passwordSettings, secret }, app } = settings; -export const login = (req: any, res: any, next: any) => { - passport.authenticate('local', { session: session.enabled }, (err, user, info) => { - if (err || !user) { - return res.status(400).json({ - errors: { - message: info ? info.message : 'Login failed' - } - }); - } +export const login = async (req: any, res: any, next: any) => { + const { + locals: { appContext } + } = res; + const { usernameOrEmail, password } = req.body; - req.login(user, { session: session.enabled }, async (loginErr: any) => { - if (loginErr) { - res.send(loginErr); - } - const tokens = jwtSetting.enabled ? await access.grantAccess(user, req, user.passwordHash) : null; - - return res.json({ user, tokens }); - }); - })(req, res, next); + appContext.auth && appContext.auth.loginMiddleware + ? res.locals.appContext.auth.loginMiddleware(req, res, next) + : res.json(await appContext.user.validateLogin(usernameOrEmail, password)); }; export const register = async ({ body, t }: any, res: any) => { @@ -62,7 +50,7 @@ export const register = async ({ body, t }: any, res: any) => { let userId = 0; if (!emailExists) { const passwordHash = await createPasswordHash(body.password); - const isActive = !password.requireEmailConfirmation; + const isActive = !passwordSettings.requireEmailConfirmation; [userId] = await userDAO.register({ ...body, isActive }, passwordHash); // if user has previously logged with facebook auth @@ -73,7 +61,7 @@ export const register = async ({ body, t }: any, res: any) => { const user = (await userDAO.getUser(userId)) as UserShape; - if (mailer && password.requireEmailConfirmation && !emailExists) { + if (mailer && passwordSettings.requireEmailConfirmation && !emailExists) { // async email jwt.sign({ identity: pick(user, 'id') }, secret, { expiresIn: '1d' }, (err, emailToken) => { const encodedToken = Buffer.from(emailToken).toString('base64'); @@ -130,8 +118,8 @@ export const resetPassword = async ({ body, t }: any, res: any) => { errors.password = t('user:auth.password.passwordsIsNotMatch'); } - if (reset.password.length < password.minLength) { - errors.password = t('user:auth.password.passwordLength', { length: password.minLength }); + if (reset.password.length < passwordSettings.minLength) { + errors.password = t('user:auth.password.passwordLength', { length: passwordSettings.minLength }); } if (!isEmpty(errors)) { @@ -154,7 +142,7 @@ export const resetPassword = async ({ body, t }: any, res: any) => { const url = `${__WEBSITE_URL__}/profile`; res.send(t('user:auth.password.resestPassword')); - if (mailer && password.sendPasswordChangesEmail) { + if (mailer && passwordSettings.sendPasswordChangesEmail) { mailer.sendMail({ from: `${app.name} <${process.env.EMAIL_USER}>`, to: identity.email, From 7e85182656ae94673c9fdb071c55c6c1393dd40f Mon Sep 17 00:00:00 2001 From: Alexandr Belokur Date: Tue, 14 May 2019 15:20:43 +0300 Subject: [PATCH 053/104] Update session accessMiddleware response --- modules/authentication/server-ts/access/session/index.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/modules/authentication/server-ts/access/session/index.ts b/modules/authentication/server-ts/access/session/index.ts index 78f438f..a1a226c 100644 --- a/modules/authentication/server-ts/access/session/index.ts +++ b/modules/authentication/server-ts/access/session/index.ts @@ -27,7 +27,13 @@ const beforeware = (app: Express) => { }; const accessMiddleware = (req: Request, res: Response, next: any) => - req.isAuthenticated() ? next() : res.send('unauthorized'); + req.isAuthenticated() + ? next() + : res.send({ + errors: { + message: 'unauthorized' + } + }); const loginMiddleware = (req: any, res: any, next: any) => { passport.authenticate('local', { session: settings.auth.session.enabled }, (err, user, info) => { From aedd4fad327c581ec0064efd8e0b055bbb412202 Mon Sep 17 00:00:00 2001 From: Ivan Isakov Date: Tue, 14 May 2019 17:54:25 +0300 Subject: [PATCH 054/104] Add common redux middleware for async operation and middleware in jwt module --- config/auth.js | 2 +- .../client-react/access/jwt/index.ts | 80 +++++++------------ modules/core/common/createReduxStore.ts | 43 +++++++++- modules/forms/client-react/FieldAdapter.jsx | 1 - modules/user/client-react/actions/users.ts | 4 +- .../user/client-react/containers/AuthBase.tsx | 4 +- .../containers/DataRootComponent.tsx | 7 +- modules/user/client-react/reducers/index.ts | 6 +- 8 files changed, 81 insertions(+), 66 deletions(-) diff --git a/config/auth.js b/config/auth.js index 61fe596..5371613 100644 --- a/config/auth.js +++ b/config/auth.js @@ -4,7 +4,7 @@ export default { enabled: true }, jwt: { - enabled: true, + enabled: false, tokenExpiresIn: '1m', refreshTokenExpiresIn: '7d' }, diff --git a/modules/authentication/client-react/access/jwt/index.ts b/modules/authentication/client-react/access/jwt/index.ts index 72e1d21..8a2184c 100644 --- a/modules/authentication/client-react/access/jwt/index.ts +++ b/modules/authentication/client-react/access/jwt/index.ts @@ -23,66 +23,42 @@ const removeTokens = async () => { await removeItem(TokensEnum.refreshToken); }; -const requestMiddleware: Middleware = ({ dispatch }) => next => action => { - const { types, callAPI, ...rest } = action; - - if (!types) { - return next(action); +const refreshAccessToken = async () => { + try { + const { data } = await axios.post(`${__API_URL__}/refreshToken`, { + refreshToken: await getItem('refreshToken') + }); + if (data) { + const { accessToken, refreshToken } = data; + await saveTokens({ accessToken, refreshToken }); + } else { + await removeTokens(); + } + } catch (e) { + await removeTokens(); + throw e; } +}; - const [REQUEST, SUCCESS, FAIL] = types; - - next({ type: REQUEST, ...rest }); +const reduxMiddleware: Middleware = ({ dispatch }) => next => action => { + const { types, callAPI, status, ...rest } = action; - const handleCallApi = async () => { + (async () => { try { - const result = await client(callAPI); - const data = result && result.data; - next({ - type: SUCCESS, - ...rest, - payload: data - }); - return data; - } catch (e) { - if (e.response && e.response.status === 401) { - return dispatch(action); + if (status === 401) { + await refreshAccessToken(); + const newAction = { ...action, status: null }; + return dispatch(newAction); } - const data = e.response && e.response.data; + return next(action); + } catch (e) { next({ - type: FAIL, - ...rest, - payload: data + type: types.FAIL, + ...rest }); throw e; } - }; - - return handleCallApi(); -}; - -const client = async (request: () => Promise) => { - try { - return await request(); - } catch (e) { - if (e.response && e.response.status === 401) { - try { - const { data } = await axios.post(`${__API_URL__}/refreshToken`, { - refreshToken: await getItem('refreshToken') - }); - if (data) { - const { accessToken, refreshToken } = data; - await saveTokens({ accessToken, refreshToken }); - } else { - await removeTokens(); - } - } catch (e) { - await removeTokens(); - throw e; - } - } - throw e; - } + })(); }; axios.interceptors.request.use(async config => { @@ -116,6 +92,6 @@ axios.interceptors.response.use(async (res: any) => { export default (settings.auth.jwt.enabled ? new AccessModule({ logout: [removeTokens], - reduxMiddleware: [requestMiddleware] + reduxMiddleware: [reduxMiddleware] }) : undefined); diff --git a/modules/core/common/createReduxStore.ts b/modules/core/common/createReduxStore.ts index 643425e..53824c8 100644 --- a/modules/core/common/createReduxStore.ts +++ b/modules/core/common/createReduxStore.ts @@ -8,6 +8,47 @@ export const getStoreReducer = (reducers: any) => ...reducers }); +const requestMiddleware: Middleware = _state => next => action => { + const { types, callAPI, ...rest } = action; + + if (!types) { + return next(action); + } + + const [REQUEST, SUCCESS, FAIL] = types; + + next({ type: REQUEST, ...rest }); + + const handleCallApi = async () => { + try { + const result = await callAPI(); + const data = result && result.data; + if (data.errors) { + throw { response: result }; + } + next({ + type: SUCCESS, + ...rest, + payload: data + }); + return data; + } catch (e) { + if (e.response && e.response.status === 401) { + return next({ ...action, status: e.response.status }); + } + const data = e.response && e.response.data; + next({ + type: FAIL, + ...rest, + payload: data + }); + throw e; + } + }; + + return handleCallApi(); +}; + const createReduxStore = ( reducers: Reducer, initialState: DeepPartial, @@ -18,7 +59,7 @@ const createReduxStore = ( const routerMiddlewares = routerMiddleware ? [routerMiddleware] : []; const reduxMiddleware = reduxMiddlewares && reduxMiddlewares.length ? reduxMiddlewares : []; - return [...routerMiddlewares, ...reduxMiddleware]; + return [...routerMiddlewares, requestMiddleware, ...reduxMiddleware]; }; return createStore( getStoreReducer(reducers), diff --git a/modules/forms/client-react/FieldAdapter.jsx b/modules/forms/client-react/FieldAdapter.jsx index c513a23..fe55444 100644 --- a/modules/forms/client-react/FieldAdapter.jsx +++ b/modules/forms/client-react/FieldAdapter.jsx @@ -46,7 +46,6 @@ class FieldAdapter extends Component { }; onChangeText = value => { - console.log('TCL: FieldAdapter -> value', value); const { formik, onChangeText, onChange, name } = this.props; if (onChange && !onChangeText) { onChange(value); diff --git a/modules/user/client-react/actions/users.ts b/modules/user/client-react/actions/users.ts index 2fa3b2a..92975d7 100644 --- a/modules/user/client-react/actions/users.ts +++ b/modules/user/client-react/actions/users.ts @@ -2,9 +2,9 @@ import axios from 'axios'; import { ActionType } from '../reducers'; import { OrderBy, Filter } from '..'; -export default function USERS(orderBy: OrderBy, filter: Filter, type = 'null') { +export default function USERS(orderBy: OrderBy, filter: Filter, type?: ActionType) { return { - types: [ActionType[type], ActionType.SET_USERS, null], + types: [type ? ActionType[type] : null, ActionType.SET_USERS, null], payload: { orderBy, filter }, callAPI: () => axios.post(`${__API_URL__}/users`, { filter, orderBy }) }; diff --git a/modules/user/client-react/containers/AuthBase.tsx b/modules/user/client-react/containers/AuthBase.tsx index 7cf99d9..087535d 100644 --- a/modules/user/client-react/containers/AuthBase.tsx +++ b/modules/user/client-react/containers/AuthBase.tsx @@ -29,9 +29,7 @@ export interface WithLogoutProps extends WithUserProps { } const withUser = (Component: React.ComponentType) => { - const WithUser = ({ currentUser, ...rest }: WithUserProps) => { - return ; - }; + const WithUser = ({ currentUser, ...rest }: WithUserProps) => ; return connect(({ user: { loading, currentUser } }: any) => ({ currentUserLoading: loading, currentUser diff --git a/modules/user/client-react/containers/DataRootComponent.tsx b/modules/user/client-react/containers/DataRootComponent.tsx index 62eac81..6482aaf 100644 --- a/modules/user/client-react/containers/DataRootComponent.tsx +++ b/modules/user/client-react/containers/DataRootComponent.tsx @@ -6,6 +6,7 @@ import Loading from '../components/Loading'; import { UserModuleState } from '../reducers'; import { CURRENT_USER } from '../actions'; import { User } from '..'; +import setting from '../../../../settings'; interface DataRootComponent { currentUser: User; @@ -18,8 +19,10 @@ const DataRootComponent: React.FunctionComponent = ({ current React.useEffect(() => { (async () => { - if (!ready && (await getItem('refreshToken')) && !currentUser) { - await getCurrentUser(); + if (!ready && !currentUser && ((await getItem('refreshToken')) || setting.auth.session.enabled)) { + try { + await getCurrentUser(); + } catch (e) {} } setReady(true); })(); diff --git a/modules/user/client-react/reducers/index.ts b/modules/user/client-react/reducers/index.ts index 61737b2..22ef04c 100644 --- a/modules/user/client-react/reducers/index.ts +++ b/modules/user/client-react/reducers/index.ts @@ -1,7 +1,6 @@ import { User, OrderBy, Filter } from '..'; export enum ActionType { - null = 'null', SET_CURRENT_USER = 'SET_CURRENT_USER', CLEAR_CURRENT_USER = 'CLEAR_CURRENT_USER', SET_LOADING = 'SET_LOADING', @@ -49,12 +48,11 @@ export default function(state = defaultState, action: UserModuleActionProps) { return { ...state, currentUser: null, - loading: false, - ...action.payload + loading: false }; case ActionType.SET_CURRENT_USER: - const currentUser = action.payload.errors ? null : (action.payload.user && action.payload) || action.payload; + const currentUser = action.payload && action.payload.errors ? null : action.payload.user || action.payload; return { ...state, currentUser, From 5b16e3f5f5f4d1b4298f2447d825d11c59224258 Mon Sep 17 00:00:00 2001 From: Ivan Isakov Date: Tue, 14 May 2019 18:01:40 +0300 Subject: [PATCH 055/104] removed extra features --- .../containers/DataRootComponent.native.tsx | 14 +++++--------- .../client-react/containers/DataRootComponent.tsx | 14 +++++--------- .../user/client-react/containers/Login.native.tsx | 6 ++---- .../user/client-react/containers/Logout.native.tsx | 2 +- modules/user/client-react/containers/Profile.tsx | 1 - .../client-react/containers/Register.native.tsx | 6 ++---- modules/user/client-react/containers/UserAdd.tsx | 10 ++++------ modules/user/client-react/containers/UserEdit.tsx | 14 ++++++-------- 8 files changed, 25 insertions(+), 42 deletions(-) diff --git a/modules/user/client-react/containers/DataRootComponent.native.tsx b/modules/user/client-react/containers/DataRootComponent.native.tsx index 160963c..d2d476b 100644 --- a/modules/user/client-react/containers/DataRootComponent.native.tsx +++ b/modules/user/client-react/containers/DataRootComponent.native.tsx @@ -30,13 +30,9 @@ class DataRootComponent extends React.Component { } } -const mapState = ({ currentUser }: UserModuleState) => ({ - currentUser -}); - -const withConnect = connect( - mapState, +export default connect( + ({ currentUser }: UserModuleState) => ({ + currentUser + }), { getCurrentUser: CURRENT_USER } -); - -export default withConnect(DataRootComponent); +)(DataRootComponent); diff --git a/modules/user/client-react/containers/DataRootComponent.tsx b/modules/user/client-react/containers/DataRootComponent.tsx index 6482aaf..d535ef4 100644 --- a/modules/user/client-react/containers/DataRootComponent.tsx +++ b/modules/user/client-react/containers/DataRootComponent.tsx @@ -30,13 +30,9 @@ const DataRootComponent: React.FunctionComponent = ({ current return ready ? children : ; }; -const mapState = ({ currentUser }: UserModuleState) => ({ - currentUser -}); - -const withConnect = connect( - mapState, +export default connect( + ({ currentUser }: UserModuleState) => ({ + currentUser + }), { getCurrentUser: CURRENT_USER } -); - -export default withConnect(DataRootComponent); +)(DataRootComponent); diff --git a/modules/user/client-react/containers/Login.native.tsx b/modules/user/client-react/containers/Login.native.tsx index 0e2bc60..06f1543 100644 --- a/modules/user/client-react/containers/Login.native.tsx +++ b/modules/user/client-react/containers/Login.native.tsx @@ -29,9 +29,7 @@ class Login extends React.Component { } } -const withConnect = connect( +export default connect( null, { login: LOGIN } -); - -export default translate('user')(withConnect(Login)); +)(translate('user')(Login)); diff --git a/modules/user/client-react/containers/Logout.native.tsx b/modules/user/client-react/containers/Logout.native.tsx index bdc21d3..56d7931 100644 --- a/modules/user/client-react/containers/Logout.native.tsx +++ b/modules/user/client-react/containers/Logout.native.tsx @@ -29,4 +29,4 @@ const LogoutView = ({ logout, t }: LogoutViewProps) => { ); }; -export default translate('user')(withLogout(LogoutView)); +export default withLogout(translate('user')(LogoutView)); diff --git a/modules/user/client-react/containers/Profile.tsx b/modules/user/client-react/containers/Profile.tsx index 09e4ed8..76802ca 100644 --- a/modules/user/client-react/containers/Profile.tsx +++ b/modules/user/client-react/containers/Profile.tsx @@ -6,7 +6,6 @@ import { User } from '..'; interface ProfileProps { currentUser: User; currentUserLoading: boolean; - error: any; } const Profile: React.FunctionComponent = props => { diff --git a/modules/user/client-react/containers/Register.native.tsx b/modules/user/client-react/containers/Register.native.tsx index 936e58b..b40e01a 100644 --- a/modules/user/client-react/containers/Register.native.tsx +++ b/modules/user/client-react/containers/Register.native.tsx @@ -54,9 +54,7 @@ class Register extends React.Component { } } -const withConnect = connect( +export default connect( null, { register: REGISTER } -); - -export default translate('user')(withConnect(Register)); +)(translate('user')(Register)); diff --git a/modules/user/client-react/containers/UserAdd.tsx b/modules/user/client-react/containers/UserAdd.tsx index 7c571f7..d682bc1 100644 --- a/modules/user/client-react/containers/UserAdd.tsx +++ b/modules/user/client-react/containers/UserAdd.tsx @@ -40,9 +40,7 @@ const UserAdd: React.FunctionComponent = props => { return ; }; -export default translate('user')( - connect( - null, - { addUser: ADD_USER } - )(UserAdd) -); +export default connect( + null, + { addUser: ADD_USER } +)(translate('user')(UserAdd)); diff --git a/modules/user/client-react/containers/UserEdit.tsx b/modules/user/client-react/containers/UserEdit.tsx index 95e04d2..54a1536 100644 --- a/modules/user/client-react/containers/UserEdit.tsx +++ b/modules/user/client-react/containers/UserEdit.tsx @@ -58,11 +58,9 @@ class UserEdit extends React.Component { } } -export default translate('user')( - connect( - ({ user: { user } }: any) => ({ - user - }), - { getUser: USER, editUser: EDIT_USER } - )(UserEdit) -); +export default connect( + ({ user: { user } }: any) => ({ + user + }), + { getUser: USER, editUser: EDIT_USER } +)(translate('user')(UserEdit)); From 4e6c08f7cc26703962e11523d10eacbb6424efd6 Mon Sep 17 00:00:00 2001 From: Ivan Isakov Date: Tue, 14 May 2019 18:04:00 +0300 Subject: [PATCH 056/104] Reaname callAPI to APICall --- modules/authentication/client-react/access/jwt/index.ts | 2 +- modules/core/common/createReduxStore.ts | 8 ++++---- modules/user/client-react/actions/addUser.ts | 2 +- modules/user/client-react/actions/currentUser.ts | 2 +- modules/user/client-react/actions/deleteUser.ts | 2 +- modules/user/client-react/actions/editUser.ts | 2 +- modules/user/client-react/actions/login.ts | 2 +- modules/user/client-react/actions/register.ts | 2 +- modules/user/client-react/actions/user.ts | 2 +- modules/user/client-react/actions/users.ts | 2 +- 10 files changed, 13 insertions(+), 13 deletions(-) diff --git a/modules/authentication/client-react/access/jwt/index.ts b/modules/authentication/client-react/access/jwt/index.ts index 8a2184c..e76810a 100644 --- a/modules/authentication/client-react/access/jwt/index.ts +++ b/modules/authentication/client-react/access/jwt/index.ts @@ -41,7 +41,7 @@ const refreshAccessToken = async () => { }; const reduxMiddleware: Middleware = ({ dispatch }) => next => action => { - const { types, callAPI, status, ...rest } = action; + const { types, status, ...rest } = action; (async () => { try { diff --git a/modules/core/common/createReduxStore.ts b/modules/core/common/createReduxStore.ts index 53824c8..36b96bd 100644 --- a/modules/core/common/createReduxStore.ts +++ b/modules/core/common/createReduxStore.ts @@ -9,7 +9,7 @@ export const getStoreReducer = (reducers: any) => }); const requestMiddleware: Middleware = _state => next => action => { - const { types, callAPI, ...rest } = action; + const { types, APICall, ...rest } = action; if (!types) { return next(action); @@ -19,9 +19,9 @@ const requestMiddleware: Middleware = _state => next => action => { next({ type: REQUEST, ...rest }); - const handleCallApi = async () => { + const handleAPICall = async () => { try { - const result = await callAPI(); + const result = await APICall(); const data = result && result.data; if (data.errors) { throw { response: result }; @@ -46,7 +46,7 @@ const requestMiddleware: Middleware = _state => next => action => { } }; - return handleCallApi(); + return handleAPICall(); }; const createReduxStore = ( diff --git a/modules/user/client-react/actions/addUser.ts b/modules/user/client-react/actions/addUser.ts index fb9dff2..c8e95b7 100644 --- a/modules/user/client-react/actions/addUser.ts +++ b/modules/user/client-react/actions/addUser.ts @@ -5,6 +5,6 @@ import { User } from '..'; export default function ADD_USER(user: User) { return { types: [null, null, null] as ActionType[], - callAPI: () => axios.post(`${__API_URL__}/addUser`, { ...user }) + APICall: () => axios.post(`${__API_URL__}/addUser`, { ...user }) }; } diff --git a/modules/user/client-react/actions/currentUser.ts b/modules/user/client-react/actions/currentUser.ts index dab1a55..34bd4e5 100644 --- a/modules/user/client-react/actions/currentUser.ts +++ b/modules/user/client-react/actions/currentUser.ts @@ -4,6 +4,6 @@ import { ActionType } from '../reducers'; export default function CURRENT_USER() { return { types: [null, ActionType.SET_CURRENT_USER, ActionType.CLEAR_CURRENT_USER], - callAPI: () => axios.get(`${__API_URL__}/currentUser`) + APICall: () => axios.get(`${__API_URL__}/currentUser`) }; } diff --git a/modules/user/client-react/actions/deleteUser.ts b/modules/user/client-react/actions/deleteUser.ts index 3a9a2d9..19e69f6 100644 --- a/modules/user/client-react/actions/deleteUser.ts +++ b/modules/user/client-react/actions/deleteUser.ts @@ -4,6 +4,6 @@ import { ActionType } from '../reducers'; export default function DELETE_USER(id: number) { return { types: [null, ActionType.DELETE_USER, null] as ActionType[], - callAPI: () => axios.delete(`${__API_URL__}/deleteUser`, { data: { id } }) + APICall: () => axios.delete(`${__API_URL__}/deleteUser`, { data: { id } }) }; } diff --git a/modules/user/client-react/actions/editUser.ts b/modules/user/client-react/actions/editUser.ts index 3a12be0..d8818e4 100644 --- a/modules/user/client-react/actions/editUser.ts +++ b/modules/user/client-react/actions/editUser.ts @@ -5,6 +5,6 @@ import { User } from '..'; export default function EDIT_USER(user: User) { return { types: [null, null, null] as ActionType[], - callAPI: () => axios.post(`${__API_URL__}/editUser`, { ...user }) + APICall: () => axios.post(`${__API_URL__}/editUser`, { ...user }) }; } diff --git a/modules/user/client-react/actions/login.ts b/modules/user/client-react/actions/login.ts index d0c5544..cbaa74f 100644 --- a/modules/user/client-react/actions/login.ts +++ b/modules/user/client-react/actions/login.ts @@ -5,6 +5,6 @@ import { ActionType } from '../reducers'; export default function LOGIN(value: LoginSubmitProps) { return { types: [null, ActionType.SET_CURRENT_USER, ActionType.CLEAR_CURRENT_USER], - callAPI: () => axios.post(`${__API_URL__}/login`, { ...value }) + APICall: () => axios.post(`${__API_URL__}/login`, { ...value }) }; } diff --git a/modules/user/client-react/actions/register.ts b/modules/user/client-react/actions/register.ts index 461a872..5a2686e 100644 --- a/modules/user/client-react/actions/register.ts +++ b/modules/user/client-react/actions/register.ts @@ -4,6 +4,6 @@ import { RegisterSubmitProps } from '..'; export default function REGISTER(value: RegisterSubmitProps) { return { types: [null, null, null] as any, - callAPI: () => axios.post(`${__API_URL__}/register`, { ...value }) + APICall: () => axios.post(`${__API_URL__}/register`, { ...value }) }; } diff --git a/modules/user/client-react/actions/user.ts b/modules/user/client-react/actions/user.ts index f6754b1..3047bdf 100644 --- a/modules/user/client-react/actions/user.ts +++ b/modules/user/client-react/actions/user.ts @@ -4,6 +4,6 @@ import { ActionType } from '../reducers'; export default function USER(id: number) { return { types: [null, ActionType.SET_USER, null], - callAPI: () => axios.get(`${__API_URL__}/user/${id}`) + APICall: () => axios.get(`${__API_URL__}/user/${id}`) }; } diff --git a/modules/user/client-react/actions/users.ts b/modules/user/client-react/actions/users.ts index 92975d7..8c7c851 100644 --- a/modules/user/client-react/actions/users.ts +++ b/modules/user/client-react/actions/users.ts @@ -6,6 +6,6 @@ export default function USERS(orderBy: OrderBy, filter: Filter, type?: ActionTyp return { types: [type ? ActionType[type] : null, ActionType.SET_USERS, null], payload: { orderBy, filter }, - callAPI: () => axios.post(`${__API_URL__}/users`, { filter, orderBy }) + APICall: () => axios.post(`${__API_URL__}/users`, { filter, orderBy }) }; } From f58a58e1e89ebfe156a3cfc57305edd20cbcf6cf Mon Sep 17 00:00:00 2001 From: Ivan Isakov Date: Wed, 15 May 2019 08:42:05 +0300 Subject: [PATCH 057/104] Fix prew commit --- modules/user/client-react/containers/Login.native.tsx | 4 ++-- modules/user/client-react/containers/Login.tsx | 2 +- modules/user/client-react/containers/Logout.native.tsx | 2 +- modules/user/client-react/containers/Register.native.tsx | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/user/client-react/containers/Login.native.tsx b/modules/user/client-react/containers/Login.native.tsx index 06f1543..99aba46 100644 --- a/modules/user/client-react/containers/Login.native.tsx +++ b/modules/user/client-react/containers/Login.native.tsx @@ -8,7 +8,7 @@ import { CommonProps, LoginSubmitProps } from '../index.native'; import { LOGIN } from '../actions'; export interface LoginProps extends CommonProps { - login: (values: LoginSubmitProps) => Promise | any; + login?: (values: LoginSubmitProps) => Promise | any; } class Login extends React.Component { @@ -29,7 +29,7 @@ class Login extends React.Component { } } -export default connect( +export default connect<{}, {}, LoginProps>( null, { login: LOGIN } )(translate('user')(Login)); diff --git a/modules/user/client-react/containers/Login.tsx b/modules/user/client-react/containers/Login.tsx index dbfc77e..e9b5c5e 100644 --- a/modules/user/client-react/containers/Login.tsx +++ b/modules/user/client-react/containers/Login.tsx @@ -10,7 +10,7 @@ import { CommonProps, LoginSubmitProps } from '..'; import { LOGIN } from '../actions'; export interface LoginProps extends CommonProps { - login: (values: LoginSubmitProps) => any; + login?: (values: LoginSubmitProps) => any; } const Login: React.FunctionComponent = props => { diff --git a/modules/user/client-react/containers/Logout.native.tsx b/modules/user/client-react/containers/Logout.native.tsx index 56d7931..bdc21d3 100644 --- a/modules/user/client-react/containers/Logout.native.tsx +++ b/modules/user/client-react/containers/Logout.native.tsx @@ -29,4 +29,4 @@ const LogoutView = ({ logout, t }: LogoutViewProps) => { ); }; -export default withLogout(translate('user')(LogoutView)); +export default translate('user')(withLogout(LogoutView)); diff --git a/modules/user/client-react/containers/Register.native.tsx b/modules/user/client-react/containers/Register.native.tsx index b40e01a..c16f1a6 100644 --- a/modules/user/client-react/containers/Register.native.tsx +++ b/modules/user/client-react/containers/Register.native.tsx @@ -9,7 +9,7 @@ import { REGISTER } from '../actions'; import { CommonProps, RegisterSubmitProps } from '../index.native'; interface RegisterProps extends CommonProps { - register: (values: RegisterSubmitProps) => any; + register?: (values: RegisterSubmitProps) => any; } interface RegisterState { @@ -54,7 +54,7 @@ class Register extends React.Component { } } -export default connect( +export default connect<{}, {}, RegisterProps>( null, { register: REGISTER } )(translate('user')(Register)); From a099c753ee63d6d24f71492ad59b6e04cebb738f Mon Sep 17 00:00:00 2001 From: Alexandr Belokur Date: Wed, 15 May 2019 10:30:55 +0300 Subject: [PATCH 058/104] Update controllers with identity check --- modules/user/server-ts/controllers.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/modules/user/server-ts/controllers.ts b/modules/user/server-ts/controllers.ts index ca243e2..1c9dd15 100644 --- a/modules/user/server-ts/controllers.ts +++ b/modules/user/server-ts/controllers.ts @@ -17,7 +17,7 @@ const { } = settings; export const user = async ({ params: { id }, user: identity, t }: any, res: any) => { - if (+identity.id === +id || identity.role === 'admin') { + if ((identity && +identity.id === +id) || identity.role === 'admin') { try { res.json(await userDAO.getUser(id)); } catch (e) { @@ -27,14 +27,15 @@ export const user = async ({ params: { id }, user: identity, t }: any, res: any) res.status(401).send(t('user:accessDenied')); } }; + export const users = async ({ body: { orderBy, filter }, user: identity, t }: any, res: any) => { - identity.role === 'admin' + true // identity && identity.role === 'admin' ? res.json(await userDAO.getUsers(orderBy, filter)) : res.status(401).send(t('user:accessDenied')); }; export const currentUser = async ({ user: identity }: any, res: any) => { - if (identity.id) { + if (identity && identity.id) { res.json(await userDAO.getUser(identity.id)); } else { res.send(null); @@ -42,7 +43,7 @@ export const currentUser = async ({ user: identity }: any, res: any) => { }; export const addUser = async ({ body, user: identity, t }: any, res: any) => { - if (identity.role !== 'admin') { + if (identity && identity.role !== 'admin') { return res.status(401).send(t('user:accessDenied')); } const errors: ValidationErrors = {}; @@ -117,8 +118,8 @@ export const addUser = async ({ body, user: identity, t }: any, res: any) => { }; export const editUser = async ({ user: identity, body, t }: any, res: any) => { - const isAdmin = () => identity.role === 'admin'; - const isSelf = () => +identity.id === +body.id; + const isAdmin = () => identity && identity.role === 'admin'; + const isSelf = () => identity && +identity.id === +body.id; if (!isSelf() && !isAdmin()) { return res.status(401).send(t('user:accessDenied')); @@ -192,8 +193,8 @@ export const editUser = async ({ user: identity, body, t }: any, res: any) => { }; export const deleteUser = async ({ user: identity, body: { id }, t }: any, res: any) => { - const isAdmin = () => identity.role === 'admin'; - const isSelf = () => +identity.id === +id; + const isAdmin = () => identity && identity.role === 'admin'; + const isSelf = () => identity && +identity.id === +id; const userData = await userDAO.getUser(id); if (!userData) { From b3ec2b177996d3a4f813d61acd1cb6d112ade788 Mon Sep 17 00:00:00 2001 From: Alexandr Belokur Date: Wed, 15 May 2019 10:32:07 +0300 Subject: [PATCH 059/104] Remove table auth_certificate from migrations --- modules/user/server-ts/migrations/002_user.js | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/modules/user/server-ts/migrations/002_user.js b/modules/user/server-ts/migrations/002_user.js index d60f829..6719073 100644 --- a/modules/user/server-ts/migrations/002_user.js +++ b/modules/user/server-ts/migrations/002_user.js @@ -21,17 +21,6 @@ exports.up = function(knex, Promise) { .onDelete('CASCADE'); table.timestamps(false, true); }), - knex.schema.createTable('auth_certificate', table => { - table.increments(); - table.string('serial').unique(); - table - .integer('user_id') - .unsigned() - .references('id') - .inTable('user') - .onDelete('CASCADE'); - table.timestamps(false, true); - }), knex.schema.createTable('auth_facebook', table => { table.increments(); table.string('fb_id').unique(); From 8095418856b6ddd256f11f192dc4e7aa907e9d0c Mon Sep 17 00:00:00 2001 From: Alexandr Belokur Date: Wed, 15 May 2019 10:36:07 +0300 Subject: [PATCH 060/104] Fix typo in user conrtollers --- modules/user/server-ts/controllers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/user/server-ts/controllers.ts b/modules/user/server-ts/controllers.ts index 1c9dd15..290e247 100644 --- a/modules/user/server-ts/controllers.ts +++ b/modules/user/server-ts/controllers.ts @@ -29,7 +29,7 @@ export const user = async ({ params: { id }, user: identity, t }: any, res: any) }; export const users = async ({ body: { orderBy, filter }, user: identity, t }: any, res: any) => { - true // identity && identity.role === 'admin' + identity && identity.role === 'admin' ? res.json(await userDAO.getUsers(orderBy, filter)) : res.status(401).send(t('user:accessDenied')); }; From c63245bea8315a747ffb272c72f3f033bff5124e Mon Sep 17 00:00:00 2001 From: Ivan Isakov Date: Wed, 15 May 2019 11:17:20 +0300 Subject: [PATCH 061/104] Add reset and forgot password components --- .../client-react/actions/forgotPassword.ts | 10 ++ modules/user/client-react/actions/index.ts | 16 ++- .../client-react/actions/resetPassword.ts | 14 ++ .../components/ForgotPasswordForm.native.tsx | 129 ++++++++++++++++++ .../components/ForgotPasswordForm.tsx | 63 +++++++++ .../components/ForgotPasswordView.native.tsx | 30 ++++ .../components/ForgotPasswordView.tsx | 38 ++++++ .../components/ResetPasswordForm.native.tsx | 82 +++++++++++ .../components/ResetPasswordForm.tsx | 63 +++++++++ .../components/ResetPasswordView.native.tsx | 25 ++++ .../components/ResetPasswordView.tsx | 34 +++++ .../containers/ForgotPassword.native.tsx | 39 ++++++ .../containers/ForgotPassword.tsx | 35 +++++ .../containers/ResetPassword.native.tsx | 36 +++++ .../client-react/containers/ResetPassword.tsx | 37 +++++ 15 files changed, 650 insertions(+), 1 deletion(-) create mode 100644 modules/user/client-react/actions/forgotPassword.ts create mode 100644 modules/user/client-react/actions/resetPassword.ts create mode 100644 modules/user/client-react/components/ForgotPasswordForm.native.tsx create mode 100644 modules/user/client-react/components/ForgotPasswordForm.tsx create mode 100644 modules/user/client-react/components/ForgotPasswordView.native.tsx create mode 100644 modules/user/client-react/components/ForgotPasswordView.tsx create mode 100644 modules/user/client-react/components/ResetPasswordForm.native.tsx create mode 100644 modules/user/client-react/components/ResetPasswordForm.tsx create mode 100644 modules/user/client-react/components/ResetPasswordView.native.tsx create mode 100644 modules/user/client-react/components/ResetPasswordView.tsx create mode 100644 modules/user/client-react/containers/ForgotPassword.native.tsx create mode 100644 modules/user/client-react/containers/ForgotPassword.tsx create mode 100644 modules/user/client-react/containers/ResetPassword.native.tsx create mode 100644 modules/user/client-react/containers/ResetPassword.tsx diff --git a/modules/user/client-react/actions/forgotPassword.ts b/modules/user/client-react/actions/forgotPassword.ts new file mode 100644 index 0000000..88104ea --- /dev/null +++ b/modules/user/client-react/actions/forgotPassword.ts @@ -0,0 +1,10 @@ +import axios from 'axios'; +import { ForgotPasswordSubmitProps } from '..'; +import { ActionType } from '../reducers'; + +export default function FORGOT_PASSWORD(value: ForgotPasswordSubmitProps) { + return { + types: [null, null, null] as ActionType[], + APICall: () => axios.post(`${__API_URL__}/forgotPassword`, { value }) + }; +} diff --git a/modules/user/client-react/actions/index.ts b/modules/user/client-react/actions/index.ts index 7dace9f..7e91e58 100644 --- a/modules/user/client-react/actions/index.ts +++ b/modules/user/client-react/actions/index.ts @@ -7,5 +7,19 @@ import USER from './user'; import DELETE_USER from './deleteUser'; import EDIT_USER from './editUser'; import ADD_USER from './addUser'; +import RESET_PASSWORD from './resetPassword'; +import FORGOT_PASSWORD from './forgotPassword'; -export { REGISTER, LOGIN, CURRENT_USER, CLEAR_USER, USERS, USER, DELETE_USER, EDIT_USER, ADD_USER }; +export { + REGISTER, + LOGIN, + CURRENT_USER, + CLEAR_USER, + USERS, + USER, + DELETE_USER, + EDIT_USER, + ADD_USER, + RESET_PASSWORD, + FORGOT_PASSWORD +}; diff --git a/modules/user/client-react/actions/resetPassword.ts b/modules/user/client-react/actions/resetPassword.ts new file mode 100644 index 0000000..ad5982c --- /dev/null +++ b/modules/user/client-react/actions/resetPassword.ts @@ -0,0 +1,14 @@ +import axios from 'axios'; +import { ResetPasswordSubmitProps } from '..'; +import { ActionType } from '../reducers'; + +interface ResetPasswordProps extends ResetPasswordSubmitProps { + token: string; +} + +export default function RESET_PASSWORD(value: ResetPasswordProps) { + return { + types: [null, null, null] as ActionType[], + APICall: () => axios.post(`${__API_URL__}/resetPassword`, { ...value }) + }; +} diff --git a/modules/user/client-react/components/ForgotPasswordForm.native.tsx b/modules/user/client-react/components/ForgotPasswordForm.native.tsx new file mode 100644 index 0000000..5ba5502 --- /dev/null +++ b/modules/user/client-react/components/ForgotPasswordForm.native.tsx @@ -0,0 +1,129 @@ +import * as React from 'react'; +import { withFormik, FormikProps } from 'formik'; +import { FontAwesome } from '@expo/vector-icons'; +import { View, StyleSheet, Text, Keyboard } from 'react-native'; +import KeyboardSpacer from 'react-native-keyboard-spacer'; +import { FieldAdapter as Field } from '@restapp/forms-client-react'; +import { RenderField, Button, primary } from '@restapp/look-client-react-native'; +import { placeholderColor, submit } from '@restapp/look-client-react-native/styles'; +import { required, email, validate } from '@restapp/validation-common-react'; +import { translate } from '@restapp/i18n-client-react'; +import { CommonProps, FormProps, ForgotPasswordSubmitProps } from '../index.native'; + +interface ForgotPasswordFormProps extends CommonProps, FormProps { + sent: boolean; +} + +const forgotPasswordFormSchema = { + email: [required, email] +}; + +const ForgotPasswordForm: React.FunctionComponent> = ({ + handleSubmit, + values, + sent, + t +}) => { + return ( + + + {sent && ( + + + + + + {t('forgotPass.form.submitMsg')} + + + )} + + + + + + + + + + + + ); +}; + +const ForgotPasswordFormWithFormik = withFormik({ + enableReinitialize: true, + mapPropsToValues: () => ({ email: '' }), + async handleSubmit(values, { setErrors, resetForm, props: { onSubmit } }) { + Keyboard.dismiss(); + await onSubmit(values) + .then(() => { + resetForm(); + }) + .catch((e: any) => { + if (e && e.errors) { + setErrors(e.errors); + } else { + throw e; + } + }); + }, + validate: values => validate(values, forgotPasswordFormSchema), + displayName: 'ForgotPasswordForm' // helps with React DevTools +}); + +const styles = StyleSheet.create({ + submit, + formContainer: { + flex: 1, + alignItems: 'stretch', + justifyContent: 'center', + paddingHorizontal: 20 + }, + form: { + flex: 2 + }, + alertWrapper: { + backgroundColor: '#d4edda', + justifyContent: 'space-between', + alignItems: 'center', + flexDirection: 'row', + flexWrap: 'wrap', + borderRadius: 5, + paddingVertical: 10 + }, + alertContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'stretch' + }, + alertTextWrapper: { + padding: 5, + flex: 20, + justifyContent: 'center', + alignItems: 'center' + }, + alertIconWrapper: { + padding: 5, + flex: 4, + justifyContent: 'center', + alignItems: 'center' + }, + alertText: { + color: '#155724', + fontSize: 20, + fontWeight: '400' + } +}); + +export default translate('user')(ForgotPasswordFormWithFormik(ForgotPasswordForm)); diff --git a/modules/user/client-react/components/ForgotPasswordForm.tsx b/modules/user/client-react/components/ForgotPasswordForm.tsx new file mode 100644 index 0000000..ef7cabb --- /dev/null +++ b/modules/user/client-react/components/ForgotPasswordForm.tsx @@ -0,0 +1,63 @@ +import * as React from 'react'; +import { withFormik, FormikProps } from 'formik'; +import { FieldAdapter as Field } from '@restapp/forms-client-react'; +import { translate } from '@restapp/i18n-client-react'; +import { Form, RenderField, Button, Alert } from '@restapp/look-client-react'; +import { required, email, validate } from '@restapp/validation-common-react'; + +import { CommonProps, FormProps, ForgotPasswordSubmitProps } from '..'; + +interface ForgotPasswordFormProps extends CommonProps, FormProps { + sent: boolean; +} + +const forgotPasswordFormSchema = { + email: [required, email] +}; + +const ForgotPasswordForm: React.FunctionComponent> = ({ + handleSubmit, + errors, + sent, + values, + t +}) => { + return ( + + {sent && {t('forgotPass.form.submitMsg')}} + +
+ {errors && errors.message && {errors.message}} + +
+ + ); +}; + +const ForgotPasswordFormWithFormik = withFormik({ + enableReinitialize: true, + mapPropsToValues: () => ({ email: '' }), + async handleSubmit(values, { setErrors, resetForm, props: { onSubmit } }) { + await onSubmit(values) + .then(() => resetForm()) + .catch((e: any) => { + if (e && e.errors) { + setErrors(e.errors); + } else { + throw e; + } + }); + }, + validate: values => validate(values, forgotPasswordFormSchema), + displayName: 'ForgotPasswordForm' // helps with React DevTools +}); + +export default translate('user')(ForgotPasswordFormWithFormik(ForgotPasswordForm)); diff --git a/modules/user/client-react/components/ForgotPasswordView.native.tsx b/modules/user/client-react/components/ForgotPasswordView.native.tsx new file mode 100644 index 0000000..8e180ac --- /dev/null +++ b/modules/user/client-react/components/ForgotPasswordView.native.tsx @@ -0,0 +1,30 @@ +import * as React from 'react'; +import { StyleSheet, View } from 'react-native'; +import ForgotPasswordForm from './ForgotPasswordForm.native'; +import { ForgotPasswordSubmitProps } from '../index.native'; + +interface ForgotPasswordViewProps { + onSubmit: (values: ForgotPasswordSubmitProps) => void; + sent: boolean; +} + +class ForgotPasswordView extends React.PureComponent { + public render() { + const { onSubmit, sent } = this.props; + return ( + + + + ); + } +} + +const styles = StyleSheet.create({ + forgotPassContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'stretch' + } +}); + +export default ForgotPasswordView; diff --git a/modules/user/client-react/components/ForgotPasswordView.tsx b/modules/user/client-react/components/ForgotPasswordView.tsx new file mode 100644 index 0000000..dff5d02 --- /dev/null +++ b/modules/user/client-react/components/ForgotPasswordView.tsx @@ -0,0 +1,38 @@ +import * as React from 'react'; +import Helmet from 'react-helmet'; +import { LayoutCenter, PageLayout } from '@restapp/look-client-react'; + +import ForgotPasswordForm from '../components/ForgotPasswordForm'; +import settings from '../../../../settings'; +import { CommonProps, ForgotPasswordSubmitProps } from '..'; + +interface ForgotPasswordViewProps extends CommonProps { + onSubmit: (values: ForgotPasswordSubmitProps) => void; + sent: boolean; +} + +const ForgotPasswordView: React.FunctionComponent = ({ onSubmit, t, sent }) => { + const renderMetaData = () => ( + + ); + + return ( + + {renderMetaData()} + +

{t('forgotPass.form.title')}

+ +
+
+ ); +}; + +export default ForgotPasswordView; diff --git a/modules/user/client-react/components/ResetPasswordForm.native.tsx b/modules/user/client-react/components/ResetPasswordForm.native.tsx new file mode 100644 index 0000000..cabc400 --- /dev/null +++ b/modules/user/client-react/components/ResetPasswordForm.native.tsx @@ -0,0 +1,82 @@ +import React from 'react'; +import { withFormik, FormikProps } from 'formik'; +import { View, StyleSheet } from 'react-native'; +import KeyboardSpacer from 'react-native-keyboard-spacer'; +import { FieldAdapter as Field } from '@restapp/forms-client-react'; +import { translate } from '@restapp/i18n-client-react'; + +import { RenderField, Button, primary } from '@restapp/look-client-react-native'; +import { placeholderColor, submit } from '@restapp/look-client-react-native/styles'; +import { required, minLength, validate, match } from '@restapp/validation-common-react'; +import settings from '../../../../settings'; +import { CommonProps, FormProps, ResetPasswordSubmitProps } from '../index.native'; + +interface ResetPasswordFormProps extends CommonProps, FormProps {} + +const resetPasswordFormSchema = { + password: [required, minLength(settings.auth.password.minLength)], + passwordConfirmation: [match('password'), required, minLength(settings.auth.password.minLength)] +}; + +const ResetPasswordForm: React.FunctionComponent> = ({ + values, + handleSubmit, + t +}) => { + return ( + + + + + + + + + ); +}; + +const ResetPasswordFormWithFormik = withFormik({ + enableReinitialize: true, + mapPropsToValues: () => ({ password: '', passwordConfirmation: '' }), + async handleSubmit(values, { setErrors, resetForm, props: { onSubmit } }) { + await onSubmit(values) + .then(() => resetForm()) + .catch((e: any) => { + if (e && e.errors) { + setErrors(e.errors); + } else { + throw e; + } + }); + }, + validate: values => validate(values, resetPasswordFormSchema), + displayName: 'LoginForm' // helps with React DevTools +}); + +const styles = StyleSheet.create({ + submit, + formContainer: { + paddingHorizontal: 20, + justifyContent: 'center' + } +}); + +export default translate('user')(ResetPasswordFormWithFormik(ResetPasswordForm)); diff --git a/modules/user/client-react/components/ResetPasswordForm.tsx b/modules/user/client-react/components/ResetPasswordForm.tsx new file mode 100644 index 0000000..94330d0 --- /dev/null +++ b/modules/user/client-react/components/ResetPasswordForm.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { withFormik, FormikProps } from 'formik'; +import { FieldAdapter as Field } from '@restapp/forms-client-react'; +import { translate } from '@restapp/i18n-client-react'; +import { required, minLength, validate, match } from '@restapp/validation-common-react'; +import { Form, RenderField, Button, Alert } from '@restapp/look-client-react'; +import settings from '../../../../settings'; +import { CommonProps, FormProps, ResetPasswordSubmitProps } from '..'; + +interface ResetPasswordFormProps extends CommonProps, FormProps {} + +const resetPasswordFormSchema = { + password: [required, minLength(settings.auth.password.minLength)], + passwordConfirmation: [match('password'), required, minLength(settings.auth.password.minLength)] +}; + +const ResetPasswordForm: React.FunctionComponent> = ({ + values, + handleSubmit, + errors, + t +}) => ( +
+ + + {errors && errors.message && {errors.message}} + + +); + +const ResetPasswordFormWithFormik = withFormik({ + enableReinitialize: true, + mapPropsToValues: () => ({ password: '', passwordConfirmation: '' }), + async handleSubmit(values, { setErrors, resetForm, props: { onSubmit } }) { + await onSubmit(values) + .then(() => resetForm()) + .catch((e: any) => { + if (e && e.errors) { + setErrors(e.errors); + } else { + throw e; + } + }); + }, + validate: values => validate(values, resetPasswordFormSchema), + displayName: 'LoginForm' // helps with React DevTools +}); + +export default translate('user')(ResetPasswordFormWithFormik(ResetPasswordForm)); diff --git a/modules/user/client-react/components/ResetPasswordView.native.tsx b/modules/user/client-react/components/ResetPasswordView.native.tsx new file mode 100644 index 0000000..6c594ff --- /dev/null +++ b/modules/user/client-react/components/ResetPasswordView.native.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { View, StyleSheet } from 'react-native'; + +import ResetPasswordForm from '../components/ResetPasswordForm.native'; +import { ResetPasswordSubmitProps } from '../index.native'; + +interface ResetPasswordViewProps { + onSubmit: (values: ResetPasswordSubmitProps) => void; +} + +const ResetPasswordView: React.FunctionComponent = ({ onSubmit }) => ( + + + +); + +const styles = StyleSheet.create({ + container: { + justifyContent: 'center', + alignItems: 'stretch', + flex: 1 + } +}); + +export default ResetPasswordView; diff --git a/modules/user/client-react/components/ResetPasswordView.tsx b/modules/user/client-react/components/ResetPasswordView.tsx new file mode 100644 index 0000000..b5886aa --- /dev/null +++ b/modules/user/client-react/components/ResetPasswordView.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import Helmet from 'react-helmet'; +import { PageLayout } from '@restapp/look-client-react'; +import ResetPasswordForm from '../components/ResetPasswordForm'; +import { CommonProps, ResetPasswordSubmitProps } from '..'; +import settings from '../../../../settings'; + +interface ResetPasswordViewProps extends CommonProps { + onSubmit: (values: ResetPasswordSubmitProps) => void; +} + +const ResetPasswordView: React.FunctionComponent = ({ t, onSubmit }) => { + const renderMetaData = () => ( + + ); + + return ( + + {renderMetaData()} +

{t('resetPass.form.title')}

+ +
+ ); +}; + +export default ResetPasswordView; diff --git a/modules/user/client-react/containers/ForgotPassword.native.tsx b/modules/user/client-react/containers/ForgotPassword.native.tsx new file mode 100644 index 0000000..ed595d7 --- /dev/null +++ b/modules/user/client-react/containers/ForgotPassword.native.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { translate } from '@restapp/i18n-client-react'; +import { FormError } from '@restapp/forms-client-react'; +import ForgotPasswordView from '../components/ForgotPasswordView.native'; +import { CommonProps, User, ForgotPasswordSubmitProps } from '../index.native'; +import { FORGOT_PASSWORD } from '../actions'; + +interface ForgotPasswordProps extends CommonProps { + forgotPassword: (values: ForgotPasswordSubmitProps) => any; +} + +class ForgotPassword extends React.Component { + public state = { + sent: false + }; + + public onSubmit = async (values: User) => { + const { t, forgotPassword } = this.props; + + this.setState({ sent: true }); + try { + await forgotPassword(values); + } catch (e) { + throw new FormError(t('forgotPass.errorMsg'), e); + } + }; + + public render() { + const { sent } = this.state; + + return ; + } +} + +export default connect( + null, + { forgotPassword: FORGOT_PASSWORD } +)(translate('user')(ForgotPassword)); diff --git a/modules/user/client-react/containers/ForgotPassword.tsx b/modules/user/client-react/containers/ForgotPassword.tsx new file mode 100644 index 0000000..38c8598 --- /dev/null +++ b/modules/user/client-react/containers/ForgotPassword.tsx @@ -0,0 +1,35 @@ +import React, { useState } from 'react'; +import { connect } from 'react-redux'; +import { translate } from '@restapp/i18n-client-react'; +import { FormError } from '@restapp/forms-client-react'; +import ForgotPasswordView from '../components/ForgotPasswordView'; + +import { CommonProps, ForgotPasswordSubmitProps } from '..'; + +import { FORGOT_PASSWORD } from '../actions'; + +interface ForgotPasswordProps extends CommonProps { + forgotPassword: (values: ForgotPasswordSubmitProps) => any; +} + +const ForgotPassword: React.FunctionComponent = props => { + const { t, forgotPassword } = props; + + const [sent, setSent] = useState(false); + + const onSubmit = async (values: ForgotPasswordSubmitProps) => { + setSent(true); + try { + await forgotPassword(values); + } catch (e) { + throw new FormError(t('forgotPass.errorMsg'), e); + } + }; + + return ; +}; + +export default connect( + null, + { forgotPassword: FORGOT_PASSWORD } +)(translate('user')(ForgotPassword)); diff --git a/modules/user/client-react/containers/ResetPassword.native.tsx b/modules/user/client-react/containers/ResetPassword.native.tsx new file mode 100644 index 0000000..2e85b3d --- /dev/null +++ b/modules/user/client-react/containers/ResetPassword.native.tsx @@ -0,0 +1,36 @@ +import * as React from 'react'; +import { connect } from 'react-redux'; +import { translate } from '@restapp/i18n-client-react'; +import { FormError } from '@restapp/forms-client-react'; +import ResetPasswordView from '../components/ResetPasswordView.native'; +import { RESET_PASSWORD } from '../actions'; +import { CommonProps, ResetPasswordSubmitProps } from '../index.native'; + +interface Token { + token: string; +} + +interface ResetPasswordProps extends CommonProps { + resetPassword: (value: ResetPasswordSubmitProps & Token) => void; + match: any; +} + +const ResetPassword: React.FunctionComponent = props => { + const { t, resetPassword, navigation, match } = props; + + const onSubmit = async (values: ResetPasswordSubmitProps) => { + try { + await resetPassword({ ...values, token: match.params.token }); + } catch (e) { + throw new FormError(t('resetPass.errorMsg'), e); + } + navigation.navigate('Login'); + }; + + return ; +}; + +export default connect( + null, + { resetPassword: RESET_PASSWORD } +)(translate('user')(ResetPassword)); diff --git a/modules/user/client-react/containers/ResetPassword.tsx b/modules/user/client-react/containers/ResetPassword.tsx new file mode 100644 index 0000000..76905c5 --- /dev/null +++ b/modules/user/client-react/containers/ResetPassword.tsx @@ -0,0 +1,37 @@ +import * as React from 'react'; +import { connect } from 'react-redux'; +import { translate } from '@restapp/i18n-client-react'; +import { FormError } from '@restapp/forms-client-react'; +import ResetPasswordView from '../components/ResetPasswordView'; + +import { CommonProps, ResetPasswordSubmitProps } from '..'; +import { RESET_PASSWORD } from '../actions'; + +interface Token { + token: string; +} + +interface ResetPasswordProps extends CommonProps { + resetPassword: (value: ResetPasswordSubmitProps & Token) => void; + match: any; +} + +const ResetPassword: React.FunctionComponent = props => { + const { t, resetPassword, history, match } = props; + + const onSubmit = async (values: ResetPasswordSubmitProps) => { + try { + await resetPassword({ ...values, token: match.params.token }); + } catch (e) { + throw new FormError(t('resetPass.errorMsg'), e); + } + history.push('/login'); + }; + + return ; +}; + +export default connect( + null, + { resetPassword: RESET_PASSWORD } +)(translate('user')(ResetPassword)); From d43956b71e4b3095435e50f1e94069cc9042d891 Mon Sep 17 00:00:00 2001 From: Ivan Isakov Date: Wed, 15 May 2019 11:21:47 +0300 Subject: [PATCH 062/104] Add routes and navigations screens for forgot and reset password --- .../containers/ForgotPassword.native.tsx | 4 ++-- .../containers/ResetPassword.native.tsx | 6 ++--- modules/user/client-react/index.native.tsx | 24 +++++++++++++++++++ modules/user/client-react/index.tsx | 6 ++++- 4 files changed, 34 insertions(+), 6 deletions(-) diff --git a/modules/user/client-react/containers/ForgotPassword.native.tsx b/modules/user/client-react/containers/ForgotPassword.native.tsx index ed595d7..d604c84 100644 --- a/modules/user/client-react/containers/ForgotPassword.native.tsx +++ b/modules/user/client-react/containers/ForgotPassword.native.tsx @@ -7,7 +7,7 @@ import { CommonProps, User, ForgotPasswordSubmitProps } from '../index.native'; import { FORGOT_PASSWORD } from '../actions'; interface ForgotPasswordProps extends CommonProps { - forgotPassword: (values: ForgotPasswordSubmitProps) => any; + forgotPassword?: (values: ForgotPasswordSubmitProps) => any; } class ForgotPassword extends React.Component { @@ -33,7 +33,7 @@ class ForgotPassword extends React.Component { } } -export default connect( +export default connect<{}, {}, ForgotPasswordProps>( null, { forgotPassword: FORGOT_PASSWORD } )(translate('user')(ForgotPassword)); diff --git a/modules/user/client-react/containers/ResetPassword.native.tsx b/modules/user/client-react/containers/ResetPassword.native.tsx index 2e85b3d..077ff2f 100644 --- a/modules/user/client-react/containers/ResetPassword.native.tsx +++ b/modules/user/client-react/containers/ResetPassword.native.tsx @@ -11,8 +11,8 @@ interface Token { } interface ResetPasswordProps extends CommonProps { - resetPassword: (value: ResetPasswordSubmitProps & Token) => void; - match: any; + resetPassword?: (value: ResetPasswordSubmitProps & Token) => void; + match?: any; } const ResetPassword: React.FunctionComponent = props => { @@ -30,7 +30,7 @@ const ResetPassword: React.FunctionComponent = props => { return ; }; -export default connect( +export default connect<{}, {}, ResetPasswordProps>( null, { resetPassword: RESET_PASSWORD } )(translate('user')(ResetPassword)); diff --git a/modules/user/client-react/index.native.tsx b/modules/user/client-react/index.native.tsx index 17a5369..0ce58ab 100644 --- a/modules/user/client-react/index.native.tsx +++ b/modules/user/client-react/index.native.tsx @@ -16,6 +16,8 @@ import UserScreenNavigator from './containers/UserScreenNavigator.native'; import Login from './containers/Login.native'; import Logout from './containers/Logout.native'; import Register from './containers/Register.native'; +import ForgotPassword from './containers/ForgotPassword.native'; +import ResetPassword from './containers/ResetPassword.native'; import reducers from './reducers'; @@ -131,9 +133,31 @@ class RegisterScreen extends React.Component { } } +class ForgotPasswordScreen extends React.Component { + public static navigationOptions = () => ({ + headerTitle: , + headerForceInset: {} + }); + public render() { + return ; + } +} + +class ResetPasswordScreen extends React.Component { + public static navigationOptions = () => ({ + headerTitle: , + headerForceInset: {} + }); + public render() { + return ; + } +} + const AuthScreen = createStackNavigator( { Login: { screen: LoginScreen }, + ForgotPassword: { screen: ForgotPasswordScreen }, + ResetPassword: { screen: ResetPasswordScreen }, Register: { screen: RegisterScreen } }, { diff --git a/modules/user/client-react/index.tsx b/modules/user/client-react/index.tsx index 0f05a53..d713a3f 100644 --- a/modules/user/client-react/index.tsx +++ b/modules/user/client-react/index.tsx @@ -15,6 +15,8 @@ import Users from './containers/Users'; import UserEdit from './containers/UserEdit'; import UserAdd from './containers/UserAdd'; import Profile from './containers/Profile'; +import ForgotPassword from './containers/ForgotPassword'; +import ResetPassword from './containers/ResetPassword'; import reducers from './reducers'; import { AuthRoute, IfLoggedIn, IfNotLoggedIn, withLoadedUser, withLogout, WithLogoutProps } from './containers/Auth'; @@ -142,7 +144,9 @@ export default new ClientModule({ , , , - + , + , + ], navItem: [ From 3f21abdf502c6ae67cca32941c093d59578223ee Mon Sep 17 00:00:00 2001 From: Alexandr Belokur Date: Wed, 15 May 2019 15:20:22 +0300 Subject: [PATCH 063/104] Add social to user and auth modules --- config/auth.js | 18 ++--- .../facebook/containers/FacebookButton.tsx | 2 +- .../social/github/containers/GitHubButton.tsx | 2 +- .../social/google/containers/GoogleButton.tsx | 2 +- .../linkedin/containers/LinkedInButton.tsx | 2 +- modules/authentication/server-ts/index.ts | 6 +- .../server-ts/social/AuthModule.ts | 13 ++++ .../server-ts/social/facebook/controllers.ts | 15 +++++ .../server-ts/social/facebook/index.ts | 51 ++++++++++++++ .../server-ts/social/github/controllers.ts | 15 +++++ .../server-ts/social/github/index.ts | 51 ++++++++++++++ .../server-ts/social/google/controllers.ts | 25 +++++++ .../server-ts/social/google/index.ts | 53 +++++++++++++++ .../authentication/server-ts/social/index.ts | 7 ++ .../server-ts/social/linkedIn/controllers.ts | 15 +++++ .../server-ts/social/linkedIn/index.ts | 51 ++++++++++++++ modules/user/server-ts/index.ts | 3 +- .../user/server-ts/social/facebook/index.ts | 40 +++++++++++ modules/user/server-ts/social/github/index.ts | 41 ++++++++++++ modules/user/server-ts/social/google/index.ts | 66 +++++++++++++++++++ modules/user/server-ts/social/index.ts | 16 +++++ .../user/server-ts/social/linkedIn/index.ts | 36 ++++++++++ modules/user/server-ts/social/shared.ts | 28 ++++++++ 23 files changed, 542 insertions(+), 16 deletions(-) create mode 100644 modules/authentication/server-ts/social/AuthModule.ts create mode 100644 modules/authentication/server-ts/social/facebook/controllers.ts create mode 100644 modules/authentication/server-ts/social/facebook/index.ts create mode 100644 modules/authentication/server-ts/social/github/controllers.ts create mode 100644 modules/authentication/server-ts/social/github/index.ts create mode 100644 modules/authentication/server-ts/social/google/controllers.ts create mode 100644 modules/authentication/server-ts/social/google/index.ts create mode 100644 modules/authentication/server-ts/social/index.ts create mode 100644 modules/authentication/server-ts/social/linkedIn/controllers.ts create mode 100644 modules/authentication/server-ts/social/linkedIn/index.ts create mode 100644 modules/user/server-ts/social/facebook/index.ts create mode 100644 modules/user/server-ts/social/github/index.ts create mode 100644 modules/user/server-ts/social/google/index.ts create mode 100644 modules/user/server-ts/social/index.ts create mode 100644 modules/user/server-ts/social/linkedIn/index.ts create mode 100644 modules/user/server-ts/social/shared.ts diff --git a/config/auth.js b/config/auth.js index 61fe596..a941ab9 100644 --- a/config/auth.js +++ b/config/auth.js @@ -16,32 +16,32 @@ export default { }, social: { facebook: { - enabled: false, + enabled: true, clientID: process.env.FACEBOOK_CLIENTID, clientSecret: process.env.FACEBOOK_CLIENTSECRET, - callbackURL: '/auth/facebook/callback', + callbackURL: '/api/auth/facebook/callback', scope: ['email'], profileFields: ['id', 'emails', 'displayName'] }, github: { - enabled: false, + enabled: true, clientID: process.env.GITHUB_CLIENTID, clientSecret: process.env.GITHUB_CLIENTSECRET, - callbackURL: '/auth/github/callback', + callbackURL: '/api/auth/github/callback', scope: ['user:email'] }, linkedin: { - enabled: false, + enabled: true, clientID: process.env.LINKEDIN_CLIENTID, clientSecret: process.env.LINKEDIN_CLIENTSECRET, - callbackURL: '/auth/linkedin/callback', - scope: ['r_emailaddress', 'r_basicprofile'] + callbackURL: '/api/auth/linkedin/callback', + scope: ['r_liteprofile'] }, google: { - enabled: false, + enabled: true, clientID: process.env.GOOGLE_CLIENTID, clientSecret: process.env.GOOGLE_CLIENTSECRET, - callbackURL: '/auth/google/callback', + callbackURL: '/api/auth/google/callback', scope: ['https://www.googleapis.com/auth/userinfo.email', 'https://www.googleapis.com/auth/userinfo.profile'] } } diff --git a/modules/authentication/client-react/social/facebook/containers/FacebookButton.tsx b/modules/authentication/client-react/social/facebook/containers/FacebookButton.tsx index 5d8ada5..9f654c2 100644 --- a/modules/authentication/client-react/social/facebook/containers/FacebookButton.tsx +++ b/modules/authentication/client-react/social/facebook/containers/FacebookButton.tsx @@ -8,7 +8,7 @@ import { SocialButton, SocialButtonComponent } from '../../..'; import './FacebookButton.css'; const facebookLogin = () => { - window.location.href = '/auth/facebook'; + window.location.href = '/api/auth/facebook'; }; const FacebookButton = ({ text }: SocialButtonComponent) => { diff --git a/modules/authentication/client-react/social/github/containers/GitHubButton.tsx b/modules/authentication/client-react/social/github/containers/GitHubButton.tsx index a459525..97285a2 100644 --- a/modules/authentication/client-react/social/github/containers/GitHubButton.tsx +++ b/modules/authentication/client-react/social/github/containers/GitHubButton.tsx @@ -8,7 +8,7 @@ import { SocialButton, SocialButtonComponent } from '../..'; import './GitHubButton.css'; const githubLogin = () => { - window.location.href = '/auth/github'; + window.location.href = '/api/auth/github'; }; const GitHubButton = ({ text }: SocialButtonComponent) => { diff --git a/modules/authentication/client-react/social/google/containers/GoogleButton.tsx b/modules/authentication/client-react/social/google/containers/GoogleButton.tsx index 168482c..dce2914 100644 --- a/modules/authentication/client-react/social/google/containers/GoogleButton.tsx +++ b/modules/authentication/client-react/social/google/containers/GoogleButton.tsx @@ -8,7 +8,7 @@ import { SocialButtonComponent, SocialButton } from '../..'; import './GoogleButton.css'; const googleLogin = () => { - window.location.href = '/auth/google'; + window.location.href = '/api/auth/google'; }; const GoogleButton = ({ text }: SocialButtonComponent) => { diff --git a/modules/authentication/client-react/social/linkedin/containers/LinkedInButton.tsx b/modules/authentication/client-react/social/linkedin/containers/LinkedInButton.tsx index 696c6eb..f32c1ab 100644 --- a/modules/authentication/client-react/social/linkedin/containers/LinkedInButton.tsx +++ b/modules/authentication/client-react/social/linkedin/containers/LinkedInButton.tsx @@ -8,7 +8,7 @@ import { SocialButtonComponent, SocialButton } from '../..'; import './LinkedInButton.css'; const linkedInLogin = () => { - window.location.href = '/auth/linkedin'; + window.location.href = '/api/auth/linkedin'; }; const LinkedInButton = ({ text }: SocialButtonComponent) => { diff --git a/modules/authentication/server-ts/index.ts b/modules/authentication/server-ts/index.ts index 62f2a4d..29e12d5 100644 --- a/modules/authentication/server-ts/index.ts +++ b/modules/authentication/server-ts/index.ts @@ -1,5 +1,7 @@ import ServerModule from '@restapp/module-server-ts'; +import AuthModule from './social/AuthModule'; import access from './access'; +import social from './social'; -export default new ServerModule(access); -export { access }; +export default new ServerModule(access, social); +export { access, AuthModule }; diff --git a/modules/authentication/server-ts/social/AuthModule.ts b/modules/authentication/server-ts/social/AuthModule.ts new file mode 100644 index 0000000..4cf31cc --- /dev/null +++ b/modules/authentication/server-ts/social/AuthModule.ts @@ -0,0 +1,13 @@ +import ServerModule, { ServerModuleShape } from '@restapp/module-server-ts'; + +interface AuthModuleShape extends ServerModuleShape {} + +interface AuthModule extends AuthModuleShape {} + +class AuthModule extends ServerModule { + constructor(...modules: AuthModuleShape[]) { + super(...modules); + } +} + +export default AuthModule; diff --git a/modules/authentication/server-ts/social/facebook/controllers.ts b/modules/authentication/server-ts/social/facebook/controllers.ts new file mode 100644 index 0000000..37ee72c --- /dev/null +++ b/modules/authentication/server-ts/social/facebook/controllers.ts @@ -0,0 +1,15 @@ +import passport from 'passport'; + +export const auth = (req: any, res: any) => { + passport.authenticate('facebook', { state: req.query.expoUrl })(req, res); +}; + +export const onAuthenticationSuccess = (req: any, res: any) => { + const { + locals: { appContext } + } = res; + const { t } = req; + appContext.social + ? appContext.social.facebook.onAuthenticationSuccess(req, res) + : res.status(500).send(t('auth:social')); +}; diff --git a/modules/authentication/server-ts/social/facebook/index.ts b/modules/authentication/server-ts/social/facebook/index.ts new file mode 100644 index 0000000..9b21f35 --- /dev/null +++ b/modules/authentication/server-ts/social/facebook/index.ts @@ -0,0 +1,51 @@ +import { Express } from 'express'; +import passport from 'passport'; +import { Strategy as FacebookStrategy } from 'passport-facebook'; + +import { RestMethod } from '@restapp/module-server-ts'; + +import { auth, onAuthenticationSuccess } from './controllers'; +import AuthModule from '../AuthModule'; +import settings from '../../../../../settings'; + +const { + auth: { + session, + social: { + facebook: { clientID, clientSecret, callbackURL, profileFields, enabled } + } + } +} = settings; + +const beforeware = (app: Express) => { + app.use(passport.initialize()); +}; + +const onAppCreate = ({ appContext }: AuthModule) => { + passport.use( + new FacebookStrategy( + { clientID, clientSecret, callbackURL, profileFields }, + appContext.social ? appContext.social.facebook.verifyCallback : undefined + ) + ); +}; + +export default (enabled && !__TEST__ + ? new AuthModule({ + beforeware: [beforeware], + onAppCreate: [onAppCreate], + apiRouteParams: [ + { + method: RestMethod.GET, + route: 'auth/facebook', + controller: auth + }, + { + method: RestMethod.GET, + route: 'auth/facebook/callback', + middleware: [passport.authenticate('facebook', { session: session.enabled, failureRedirect: '/login' })], + controller: onAuthenticationSuccess + } + ] + }) + : undefined); diff --git a/modules/authentication/server-ts/social/github/controllers.ts b/modules/authentication/server-ts/social/github/controllers.ts new file mode 100644 index 0000000..dcf0bd1 --- /dev/null +++ b/modules/authentication/server-ts/social/github/controllers.ts @@ -0,0 +1,15 @@ +import passport from 'passport'; + +export const auth = (req: any, res: any) => { + passport.authenticate('github', { state: req.query.expoUrl })(req, res); +}; + +export const onAuthenticationSuccess = (req: any, res: any) => { + const { + locals: { appContext } + } = res; + const { t } = req; + appContext.social + ? appContext.social.github.onAuthenticationSuccess(req, res) + : res.status(500).send(t('auth:social')); +}; diff --git a/modules/authentication/server-ts/social/github/index.ts b/modules/authentication/server-ts/social/github/index.ts new file mode 100644 index 0000000..c0e00f6 --- /dev/null +++ b/modules/authentication/server-ts/social/github/index.ts @@ -0,0 +1,51 @@ +import { Express } from 'express'; +import passport from 'passport'; +import { Strategy as GitHubStrategy } from 'passport-github'; + +import { RestMethod } from '@restapp/module-server-ts'; + +import { auth, onAuthenticationSuccess } from './controllers'; +import settings from '../../../../../settings'; +import AuthModule from '../AuthModule'; + +const { + auth: { + session, + social: { + github: { clientID, clientSecret, scope, callbackURL, enabled } + } + } +} = settings; + +const beforeware = (app: Express) => { + app.use(passport.initialize()); +}; + +const onAppCreate = ({ appContext }: AuthModule) => { + passport.use( + new GitHubStrategy( + { clientID, clientSecret, scope, callbackURL }, + appContext.social ? appContext.social.github.verifyCallback : undefined + ) + ); +}; + +export default (enabled && !__TEST__ + ? new AuthModule({ + beforeware: [beforeware], + onAppCreate: [onAppCreate], + apiRouteParams: [ + { + method: RestMethod.GET, + route: 'auth/github', + controller: auth + }, + { + method: RestMethod.GET, + route: 'auth/github/callback', + middleware: [passport.authenticate('github', { session: session.enabled, failureRedirect: '/login' })], + controller: onAuthenticationSuccess + } + ] + }) + : undefined); diff --git a/modules/authentication/server-ts/social/google/controllers.ts b/modules/authentication/server-ts/social/google/controllers.ts new file mode 100644 index 0000000..8de72c8 --- /dev/null +++ b/modules/authentication/server-ts/social/google/controllers.ts @@ -0,0 +1,25 @@ +import passport from 'passport'; + +import settings from '../../../../../settings'; + +const { + auth: { + social: { + google: { scope } + } + } +} = settings; + +export const auth = (req: any, res: any) => { + passport.authenticate('google', { scope, state: req.query.expoUrl })(req, res); +}; + +export const onAuthenticationSuccess = (req: any, res: any) => { + const { + locals: { appContext } + } = res; + const { t } = req; + appContext.social + ? appContext.social.google.onAuthenticationSuccess(req, res) + : res.status(500).send(t('auth:social')); +}; diff --git a/modules/authentication/server-ts/social/google/index.ts b/modules/authentication/server-ts/social/google/index.ts new file mode 100644 index 0000000..1f40c7c --- /dev/null +++ b/modules/authentication/server-ts/social/google/index.ts @@ -0,0 +1,53 @@ +import { Express } from 'express'; +import passport from 'passport'; +import { OAuth2Strategy as GoogleStrategy } from 'passport-google-oauth'; + +import { RestMethod } from '@restapp/module-server-ts'; + +import { auth, onAuthenticationSuccess } from './controllers'; +import AuthModule from '../AuthModule'; +import settings from '../../../../../settings'; + +const { + auth: { + session, + social: { + google: { clientID, clientSecret, callbackURL, enabled } + } + } +} = settings; + +const beforeware = (app: Express) => { + app.use(passport.initialize()); +}; + +const onAppCreate = ({ appContext }: AuthModule) => { + if (enabled && !__TEST__) { + passport.use( + new GoogleStrategy( + { clientID, clientSecret, callbackURL }, + appContext.social ? appContext.social.google.verifyCallback : undefined + ) + ); + } +}; + +export default (enabled && !__TEST__ + ? new AuthModule({ + beforeware: [beforeware], + onAppCreate: [onAppCreate], + apiRouteParams: [ + { + method: RestMethod.GET, + route: 'auth/google', + controller: auth + }, + { + method: RestMethod.GET, + route: 'auth/google/callback', + middleware: [passport.authenticate('google', { session: session.enabled, failureRedirect: '/login' })], + controller: onAuthenticationSuccess + } + ] + }) + : undefined); diff --git a/modules/authentication/server-ts/social/index.ts b/modules/authentication/server-ts/social/index.ts new file mode 100644 index 0000000..38d571a --- /dev/null +++ b/modules/authentication/server-ts/social/index.ts @@ -0,0 +1,7 @@ +import ServerModule from '@restapp/module-server-ts'; +import github from './github'; +import google from './google'; +import facebook from './facebook'; +import linkedin from './linkedIn'; + +export default new ServerModule(github, google, facebook, linkedin); diff --git a/modules/authentication/server-ts/social/linkedIn/controllers.ts b/modules/authentication/server-ts/social/linkedIn/controllers.ts new file mode 100644 index 0000000..355bae4 --- /dev/null +++ b/modules/authentication/server-ts/social/linkedIn/controllers.ts @@ -0,0 +1,15 @@ +import passport from 'passport'; + +export const auth = (req: any, res: any) => { + passport.authenticate('linkedin', { state: req.query.expoUrl })(req, res); +}; + +export const onAuthenticationSuccess = (req: any, res: any) => { + const { + locals: { appContext } + } = res; + const { t } = req; + appContext.social + ? appContext.social.google.onAuthenticationSuccess(req, res) + : res.status(500).send(t('auth:social')); +}; diff --git a/modules/authentication/server-ts/social/linkedIn/index.ts b/modules/authentication/server-ts/social/linkedIn/index.ts new file mode 100644 index 0000000..a4ce503 --- /dev/null +++ b/modules/authentication/server-ts/social/linkedIn/index.ts @@ -0,0 +1,51 @@ +import { Express } from 'express'; +import passport from 'passport'; +import { Strategy as LinkedInStrategy } from '@sokratis/passport-linkedin-oauth2'; + +import { RestMethod } from '@restapp/module-server-ts'; + +import { auth, onAuthenticationSuccess } from './controllers'; +import AuthModule from '../AuthModule'; +import settings from '../../../../../settings'; + +const { + auth: { + session, + social: { + linkedin: { clientID, clientSecret, callbackURL, enabled, scope } + } + } +} = settings; + +const beforeware = (app: Express) => { + app.use(passport.initialize()); +}; + +const onAppCreate = ({ appContext }: AuthModule) => { + passport.use( + new LinkedInStrategy( + { clientID, clientSecret, callbackURL, scopeSeparator: scope }, + appContext.social ? appContext.social.linkedin.verifyCallback : undefined + ) + ); +}; + +export default (enabled && !__TEST__ + ? new AuthModule({ + beforeware: [beforeware], + onAppCreate: [onAppCreate], + apiRouteParams: [ + { + method: RestMethod.GET, + route: 'auth/linkedin', + controller: auth + }, + { + method: RestMethod.GET, + route: 'auth/linkedin/callback', + middleware: [passport.authenticate('linkedin', { session: session.enabled, failureRedirect: '/login' })], + controller: onAuthenticationSuccess + } + ] + }) + : undefined); diff --git a/modules/user/server-ts/index.ts b/modules/user/server-ts/index.ts index 404b7ab..d564afe 100644 --- a/modules/user/server-ts/index.ts +++ b/modules/user/server-ts/index.ts @@ -5,6 +5,7 @@ import ServerModule, { RestMethod } from '@restapp/module-server-ts'; import { user, users, currentUser, addUser, editUser, deleteUser } from './controllers'; import password from './password'; +import social from './social'; import UserDAO, { UserShape } from './sql'; import settings from '../../../settings'; import resources from './locales'; @@ -48,7 +49,7 @@ const appContext = { } }; -export default new ServerModule(password, { +export default new ServerModule(password, social, { appContext, localization: [{ ns: 'user', resources }], apiRouteParams: [ diff --git a/modules/user/server-ts/social/facebook/index.ts b/modules/user/server-ts/social/facebook/index.ts new file mode 100644 index 0000000..fa23afd --- /dev/null +++ b/modules/user/server-ts/social/facebook/index.ts @@ -0,0 +1,40 @@ +import { pick } from 'lodash'; +import { AuthModule } from '@restapp/authentication-server-ts'; +import { onAuthenticationSuccess, registerUser, UserSocial } from '../shared'; +import UserDAO, { UserShape } from '../../sql'; +import settings from '../../../../../settings'; + +const createFacebookAuth = async (user: any) => UserDAO.createFacebookAuth(user); + +async function verifyCallback(accessToken: string, refreshToken: string, profile: UserSocial, cb: any) { + const { + id, + displayName, + emails: [{ value }] + } = profile; + + try { + let user = (await UserDAO.getUserByFbIdOrEmail(id, value)) as UserShape & { fbId: number }; + + if (!user) { + const [createdUserId] = await registerUser(profile); + await createFacebookAuth({ id, displayName, userId: createdUserId }); + user = (await UserDAO.getUser(createdUserId)) as UserShape & { fbId: number }; + } else if (!user.fbId) { + await createFacebookAuth({ id, displayName, userId: user.id }); + } + + return cb(null, pick(user, ['id', 'username', 'role', 'email'])); + } catch (err) { + return cb(err, {}); + } +} + +export const facebookData = { + facebook: { + onAuthenticationSuccess, + verifyCallback + } +}; + +export default (settings.auth.social.facebook.enabled && !__TEST__ ? new AuthModule() : undefined); diff --git a/modules/user/server-ts/social/github/index.ts b/modules/user/server-ts/social/github/index.ts new file mode 100644 index 0000000..e28c59f --- /dev/null +++ b/modules/user/server-ts/social/github/index.ts @@ -0,0 +1,41 @@ +import { pick } from 'lodash'; +import { AuthModule } from '@restapp/authentication-server-ts'; +import { onAuthenticationSuccess, registerUser, UserSocial } from '../shared'; +import UserDAO, { UserShape } from '../../sql'; + +import settings from '../../../../../settings'; + +const createGithubAuth = async (user: any) => UserDAO.createGithubAuth(user); + +async function verifyCallback(accessToken: string, refreshToken: string, profile: UserSocial, cb: any) { + const { + id, + displayName, + emails: [{ value }] + } = profile; + + try { + let user = (await UserDAO.getUserByGHIdOrEmail(id, value)) as UserShape & { ghId: number }; + + if (!user) { + const [createdUserId] = await registerUser(profile); + await createGithubAuth({ id, displayName, userId: createdUserId }); + user = (await UserDAO.getUser(createdUserId)) as UserShape & { ghId: number }; + } else if (!user.ghId) { + await createGithubAuth({ id, displayName, userId: user.id }); + } + + return cb(null, pick(user, ['id', 'username', 'role', 'email'])); + } catch (err) { + return cb(err, {}); + } +} + +export const githubData = { + github: { + onAuthenticationSuccess, + verifyCallback + } +}; + +export default (settings.auth.social.github.enabled && !__TEST__ ? new AuthModule() : undefined); diff --git a/modules/user/server-ts/social/google/index.ts b/modules/user/server-ts/social/google/index.ts new file mode 100644 index 0000000..cae3da2 --- /dev/null +++ b/modules/user/server-ts/social/google/index.ts @@ -0,0 +1,66 @@ +import { pick } from 'lodash'; +import { AuthModule } from '@restapp/authentication-server-ts'; +import { onAuthenticationSuccess, UserSocial } from '../shared'; +import UserDAO from '../../sql'; +import settings from '../../../../../settings'; +import { UserShape } from '@restapp/authentication-server-ts/access'; + +interface UserSocialGoogle extends UserSocial { + name: { + givenName?: string; + familyName?: string; + }; +} + +const registerUser = async ({ emails: [{ value }] }: UserSocial) => { + return UserDAO.register({ + username: value, + email: value, + isActive: true + }); +}; + +const createGoogleOAuth = async (user: any) => UserDAO.createGoogleOAuth(user); + +async function verifyCallback(accessToken: string, refreshToken: string, profile: UserSocialGoogle, cb: any) { + const { + id, + displayName, + emails: [{ value }] + } = profile; + + try { + let user = (await UserDAO.getUserByGoogleIdOrEmail(id, value)) as UserShape & { googleId: number }; + + if (!user) { + const [createdUserId] = await registerUser(profile); + + await createGoogleOAuth({ id, displayName, userId: createdUserId }); + + await UserDAO.editUserProfile({ + id: createdUserId, + profile: { + firstName: profile.name.givenName, + lastName: profile.name.familyName + } + }); + + user = (await UserDAO.getUser(createdUserId)) as UserShape & { googleId: number }; + } else if (!user.googleId) { + await createGoogleOAuth({ id, displayName, userId: user.id }); + } + + return cb(null, pick(user, ['id', 'username', 'role', 'email'])); + } catch (err) { + return cb(err, {}); + } +} + +export const googleData = { + google: { + onAuthenticationSuccess, + verifyCallback + } +}; + +export default (settings.auth.social.google.enabled && !__TEST__ ? new AuthModule() : undefined); diff --git a/modules/user/server-ts/social/index.ts b/modules/user/server-ts/social/index.ts new file mode 100644 index 0000000..011b17a --- /dev/null +++ b/modules/user/server-ts/social/index.ts @@ -0,0 +1,16 @@ +import { AuthModule } from '@restapp/authentication-server-ts'; +import facebook, { facebookData } from './facebook'; +import github, { githubData } from './github'; +import google, { googleData } from './google'; +import linkedin, { linkedinData } from './linkedIn'; + +const social = { + ...facebookData, + ...githubData, + ...googleData, + ...linkedinData +}; + +export default new AuthModule(facebook, github, google, linkedin, { + appContext: { social } +}); diff --git a/modules/user/server-ts/social/linkedIn/index.ts b/modules/user/server-ts/social/linkedIn/index.ts new file mode 100644 index 0000000..6831bba --- /dev/null +++ b/modules/user/server-ts/social/linkedIn/index.ts @@ -0,0 +1,36 @@ +import { UserShape } from './../../sql'; +import { pick } from 'lodash'; +import { AuthModule } from '@restapp/authentication-server-ts'; +import { onAuthenticationSuccess, registerUser, UserSocial } from '../shared'; +import UserDAO from '../../sql'; +import settings from '../../../../../settings'; + +const createLinkedInAuth = async (user: any) => UserDAO.createLinkedInAuth(user); + +async function verifyCallback(accessToken: string, refreshToken: string, profile: UserSocial, cb: any) { + const { id, displayName } = profile; + try { + let user = (await UserDAO.getUserByLnInIdOrEmail(id)) as UserShape & { lnId: number }; + + if (!user) { + const [createdUserId] = await registerUser(profile); + await createLinkedInAuth({ id, displayName, userId: createdUserId }); + user = (await UserDAO.getUser(createdUserId)) as UserShape & { lnId: number }; + } else if (!user.lnId) { + await createLinkedInAuth({ id, displayName, userId: user.id }); + } + + return cb(null, pick(user, ['id', 'username', 'role', 'email'])); + } catch (err) { + return cb(err, {}); + } +} + +export const linkedinData = { + linkedin: { + onAuthenticationSuccess, + verifyCallback + } +}; + +export default (settings.auth.social.linkedin.enabled && !__TEST__ ? new AuthModule() : undefined); diff --git a/modules/user/server-ts/social/shared.ts b/modules/user/server-ts/social/shared.ts new file mode 100644 index 0000000..344d568 --- /dev/null +++ b/modules/user/server-ts/social/shared.ts @@ -0,0 +1,28 @@ +import { access } from '@restapp/authentication-server-ts'; +import UserDAO, { UserShape } from '../sql'; + +export interface UserSocial { + id: number; + username: string; + displayName: string; + emails: Array<{ value: string }>; +} + +export async function onAuthenticationSuccess(req: any, res: any) { + const user = (await UserDAO.getUserWithPassword(req.user.id)) as UserShape; + const redirectUrl = req.query.state; + const tokens = await access.grantAccess(user, req, user.passwordHash); + if (redirectUrl) { + res.redirect(redirectUrl + (tokens ? '?data=' + JSON.stringify({ tokens }) : '')); + } else { + res.redirect('/profile'); + } +} + +export const registerUser = async ({ username, displayName, emails: [{ value }] = [{ value: '' }] }: UserSocial) => { + return UserDAO.register({ + username: username || displayName, + email: value, + isActive: true + }); +}; From ff7e86d2457e83a96f22e6a0aa29e7d123427e79 Mon Sep 17 00:00:00 2001 From: Alexandr Belokur Date: Wed, 15 May 2019 17:18:26 +0300 Subject: [PATCH 064/104] Fix invalid refresh token response --- .../authentication/server-ts/access/jwt/controllers.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/modules/authentication/server-ts/access/jwt/controllers.ts b/modules/authentication/server-ts/access/jwt/controllers.ts index ed769a5..004853c 100644 --- a/modules/authentication/server-ts/access/jwt/controllers.ts +++ b/modules/authentication/server-ts/access/jwt/controllers.ts @@ -19,7 +19,11 @@ export const refreshTokens = async (req: any, res: any) => { const isValidToken = decodedToken && decodedToken.id; if (!isValidToken) { - res.send(t('auth:invalidRefresh')); + return res.status(401).send({ + errors: { + message: t('auth:invalidRefresh') + } + }); } const identity = await getIdentity(decodedToken.id); @@ -29,7 +33,7 @@ export const refreshTokens = async (req: any, res: any) => { try { jwt.verify(inputRefreshToken, refreshSecret); } catch (err) { - res.send(err); + return res.send(err); } const [accessToken, refreshToken] = await createTokens(identity, settings.auth.secret, refreshSecret, req.t); From db05f63f94d3f140e31e15c2f5930c676e74cb66 Mon Sep 17 00:00:00 2001 From: Ivan Isakov Date: Thu, 16 May 2019 12:11:57 +0300 Subject: [PATCH 065/104] Divide the storage into several --- config/auth.js | 6 +- .../user/client-react/containers/AuthBase.tsx | 2 +- .../containers/DataRootComponent.native.tsx | 3 +- .../containers/DataRootComponent.tsx | 3 +- .../user/client-react/containers/Profile.tsx | 16 ++-- .../user/client-react/containers/UserEdit.tsx | 2 +- .../containers/UserOperations.tsx | 4 +- modules/user/client-react/index.native.tsx | 4 +- modules/user/client-react/index.tsx | 4 +- .../user/client-react/reducers/currentUser.ts | 46 +++++++++++ modules/user/client-react/reducers/index.ts | 79 +------------------ modules/user/client-react/reducers/users.ts | 61 ++++++++++++++ packages/server/.env | 4 +- 13 files changed, 133 insertions(+), 101 deletions(-) create mode 100644 modules/user/client-react/reducers/currentUser.ts create mode 100644 modules/user/client-react/reducers/users.ts diff --git a/config/auth.js b/config/auth.js index 28d18ab..11f648a 100644 --- a/config/auth.js +++ b/config/auth.js @@ -24,21 +24,21 @@ export default { profileFields: ['id', 'emails', 'displayName'] }, github: { - enabled: true, + enabled: false, clientID: process.env.GITHUB_CLIENTID, clientSecret: process.env.GITHUB_CLIENTSECRET, callbackURL: '/api/auth/github/callback', scope: ['user:email'] }, linkedin: { - enabled: true, + enabled: false, clientID: process.env.LINKEDIN_CLIENTID, clientSecret: process.env.LINKEDIN_CLIENTSECRET, callbackURL: '/api/auth/linkedin/callback', scope: ['r_liteprofile'] }, google: { - enabled: true, + enabled: false, clientID: process.env.GOOGLE_CLIENTID, clientSecret: process.env.GOOGLE_CLIENTSECRET, callbackURL: '/api/auth/google/callback', diff --git a/modules/user/client-react/containers/AuthBase.tsx b/modules/user/client-react/containers/AuthBase.tsx index 087535d..04ca876 100644 --- a/modules/user/client-react/containers/AuthBase.tsx +++ b/modules/user/client-react/containers/AuthBase.tsx @@ -30,7 +30,7 @@ export interface WithLogoutProps extends WithUserProps { const withUser = (Component: React.ComponentType) => { const WithUser = ({ currentUser, ...rest }: WithUserProps) => ; - return connect(({ user: { loading, currentUser } }: any) => ({ + return connect(({ currentUser: { loading, currentUser } }: any) => ({ currentUserLoading: loading, currentUser }))(WithUser); diff --git a/modules/user/client-react/containers/DataRootComponent.native.tsx b/modules/user/client-react/containers/DataRootComponent.native.tsx index d2d476b..d760074 100644 --- a/modules/user/client-react/containers/DataRootComponent.native.tsx +++ b/modules/user/client-react/containers/DataRootComponent.native.tsx @@ -3,7 +3,6 @@ import { connect } from 'react-redux'; import { getItem } from '@restapp/core-common/clientStorage'; import Loading from '../components/Loading.native'; -import { UserModuleState } from '../reducers'; import { CURRENT_USER } from '../actions'; import { User } from '..'; @@ -31,7 +30,7 @@ class DataRootComponent extends React.Component { } export default connect( - ({ currentUser }: UserModuleState) => ({ + ({ currentUser: { currentUser } }: any) => ({ currentUser }), { getCurrentUser: CURRENT_USER } diff --git a/modules/user/client-react/containers/DataRootComponent.tsx b/modules/user/client-react/containers/DataRootComponent.tsx index d535ef4..9a4078b 100644 --- a/modules/user/client-react/containers/DataRootComponent.tsx +++ b/modules/user/client-react/containers/DataRootComponent.tsx @@ -3,7 +3,6 @@ import { connect } from 'react-redux'; import { getItem } from '@restapp/core-common/clientStorage'; import Loading from '../components/Loading'; -import { UserModuleState } from '../reducers'; import { CURRENT_USER } from '../actions'; import { User } from '..'; import setting from '../../../../settings'; @@ -31,7 +30,7 @@ const DataRootComponent: React.FunctionComponent = ({ current }; export default connect( - ({ currentUser }: UserModuleState) => ({ + ({ currentUser: { currentUser } }: any) => ({ currentUser }), { getCurrentUser: CURRENT_USER } diff --git a/modules/user/client-react/containers/Profile.tsx b/modules/user/client-react/containers/Profile.tsx index 76802ca..48d6e19 100644 --- a/modules/user/client-react/containers/Profile.tsx +++ b/modules/user/client-react/containers/Profile.tsx @@ -2,19 +2,21 @@ import React from 'react'; import { connect } from 'react-redux'; import ProfileView from '../components/ProfileView'; import { User } from '..'; +import { CURRENT_USER } from '../actions'; interface ProfileProps { currentUser: User; currentUserLoading: boolean; + getCurrentUser: () => void; } -const Profile: React.FunctionComponent = props => { +const Profile: React.FunctionComponent = ({ getCurrentUser, ...props }) => { return ; }; -export default connect(({ user: { loading, currentUser } }: any) => { - return { - currentUser, - currentUserLoading: loading - }; -})(Profile); +export default connect( + ({ currentUser: { currentUser } }: any) => ({ + currentUser + }), + { getCurrentUser: CURRENT_USER } +)(Profile); diff --git a/modules/user/client-react/containers/UserEdit.tsx b/modules/user/client-react/containers/UserEdit.tsx index 54a1536..ac1d00e 100644 --- a/modules/user/client-react/containers/UserEdit.tsx +++ b/modules/user/client-react/containers/UserEdit.tsx @@ -59,7 +59,7 @@ class UserEdit extends React.Component { } export default connect( - ({ user: { user } }: any) => ({ + ({ users: { user } }: any) => ({ user }), { getUser: USER, editUser: EDIT_USER } diff --git a/modules/user/client-react/containers/UserOperations.tsx b/modules/user/client-react/containers/UserOperations.tsx index c61951e..9b8c294 100644 --- a/modules/user/client-react/containers/UserOperations.tsx +++ b/modules/user/client-react/containers/UserOperations.tsx @@ -23,7 +23,7 @@ const withUsers = (Component: React.ComponentType) => { } } return connect( - ({ user: { loading, users } }: any) => ({ + ({ users: { loading, users } }: any) => ({ loading, users }), @@ -58,7 +58,7 @@ const withSortAndFilter = (Component: React.ComponentType) => { }; return connect( - ({ user: { orderBy, filter } }: any) => ({ + ({ users: { orderBy, filter } }: any) => ({ orderBy, filter }), diff --git a/modules/user/client-react/index.native.tsx b/modules/user/client-react/index.native.tsx index 0ce58ab..6868f76 100644 --- a/modules/user/client-react/index.native.tsx +++ b/modules/user/client-react/index.native.tsx @@ -19,7 +19,7 @@ import Register from './containers/Register.native'; import ForgotPassword from './containers/ForgotPassword.native'; import ResetPassword from './containers/ResetPassword.native'; -import reducers from './reducers'; +import { currentUserReducer, usersReducer } from './reducers'; export enum UserRole { admin = 'admin', @@ -210,7 +210,7 @@ export default new ClientModule({ ], localization: [{ ns: 'user', resources }], router: , - reducer: [{ user: reducers }], + reducer: [{ currentUser: currentUserReducer, users: usersReducer }], dataRootComponent: [DataRootComponent], onAppCreate: [(module: ClientModule) => (ref.navigator = UserScreenNavigator(module.drawerItems))] }); diff --git a/modules/user/client-react/index.tsx b/modules/user/client-react/index.tsx index d713a3f..cd6b571 100644 --- a/modules/user/client-react/index.tsx +++ b/modules/user/client-react/index.tsx @@ -17,7 +17,7 @@ import UserAdd from './containers/UserAdd'; import Profile from './containers/Profile'; import ForgotPassword from './containers/ForgotPassword'; import ResetPassword from './containers/ResetPassword'; -import reducers from './reducers'; +import { currentUserReducer, usersReducer } from './reducers'; import { AuthRoute, IfLoggedIn, IfNotLoggedIn, withLoadedUser, withLogout, WithLogoutProps } from './containers/Auth'; @@ -175,7 +175,7 @@ export default new ClientModule({ ], localization: [{ ns: 'user', resources }], - reducer: [{ user: reducers }], + reducer: [{ currentUser: currentUserReducer, users: usersReducer }], dataRootComponent: [DataRootComponent], rootComponentFactory: [req => (req ? : )] }); diff --git a/modules/user/client-react/reducers/currentUser.ts b/modules/user/client-react/reducers/currentUser.ts new file mode 100644 index 0000000..1909969 --- /dev/null +++ b/modules/user/client-react/reducers/currentUser.ts @@ -0,0 +1,46 @@ +import { User } from '..'; +import { UserModuleActionProps, ActionType } from '.'; + +export enum CurrentUserActionType { + SET_CURRENT_USER = 'SET_CURRENT_USER', + CLEAR_CURRENT_USER = 'CLEAR_CURRENT_USER', + SET_LOADING = 'SET_LOADING' +} + +export interface CurrentUserState { + currentUser: User; + loading: boolean; +} + +const defaultState: CurrentUserState = { + currentUser: null, + loading: false +}; + +export default function(state = defaultState, action: UserModuleActionProps) { + switch (action.type) { + case ActionType.SET_LOADING: + return { + ...state, + loading: true + }; + + case ActionType.CLEAR_CURRENT_USER: + return { + ...state, + currentUser: null, + loading: false + }; + + case ActionType.SET_CURRENT_USER: + const currentUser = action.payload && action.payload.errors ? null : action.payload.user || action.payload; + return { + ...state, + currentUser, + loading: false + }; + + default: + return state; + } +} diff --git a/modules/user/client-react/reducers/index.ts b/modules/user/client-react/reducers/index.ts index 22ef04c..7e17c9c 100644 --- a/modules/user/client-react/reducers/index.ts +++ b/modules/user/client-react/reducers/index.ts @@ -1,4 +1,5 @@ -import { User, OrderBy, Filter } from '..'; +export { default as currentUserReducer } from './currentUser'; +export { default as usersReducer } from './users'; export enum ActionType { SET_CURRENT_USER = 'SET_CURRENT_USER', @@ -11,85 +12,9 @@ export enum ActionType { DELETE_USER = 'DELETE_USER' } -export interface UserModuleState { - currentUser: User; - loading: boolean; - user: User; - users: User[]; - orderBy: OrderBy; - filter: Filter; -} - export interface UserModuleActionProps { type: ActionType | ActionType[]; payload?: any; request?: () => Promise; [key: string]: any; } - -const defaultState: UserModuleState = { - currentUser: null, - loading: false, - user: null, - users: [], - orderBy: { column: '', order: '' }, - filter: { searchText: '', role: '', isActive: true } -}; - -export default function(state = defaultState, action: UserModuleActionProps) { - switch (action.type) { - case ActionType.SET_LOADING: - return { - ...state, - loading: true - }; - - case ActionType.CLEAR_CURRENT_USER: - return { - ...state, - currentUser: null, - loading: false - }; - - case ActionType.SET_CURRENT_USER: - const currentUser = action.payload && action.payload.errors ? null : action.payload.user || action.payload; - return { - ...state, - currentUser, - loading: false - }; - - case ActionType.SET_USER: - return { - ...state, - user: action.payload - }; - case ActionType.SET_USERS: - return { - ...state, - users: action.payload - }; - - case ActionType.SET_FILTER: - return { - ...state, - filter: action.payload && action.payload.filter - }; - - case ActionType.SET_ORDER_BY: - return { - ...state, - orderBy: action.payload && action.payload.orderBy - }; - - case ActionType.DELETE_USER: - const users = state.users.filter(user => user.id !== action.payload.id); - return { - ...state, - users - }; - - default: - return state; - } -} diff --git a/modules/user/client-react/reducers/users.ts b/modules/user/client-react/reducers/users.ts new file mode 100644 index 0000000..88ea9ba --- /dev/null +++ b/modules/user/client-react/reducers/users.ts @@ -0,0 +1,61 @@ +import { User, OrderBy, Filter } from '..'; +import { UserModuleActionProps, ActionType } from '.'; + +export interface UserModuleState { + loading: boolean; + user: User; + users: User[]; + orderBy: OrderBy; + filter: Filter; +} + +const defaultState: UserModuleState = { + loading: false, + user: null, + users: [], + orderBy: { column: '', order: '' }, + filter: { searchText: '', role: '', isActive: true } +}; + +export default function(state = defaultState, action: UserModuleActionProps) { + switch (action.type) { + case ActionType.SET_LOADING: + return { + ...state, + loading: true + }; + + case ActionType.SET_USER: + return { + ...state, + user: action.payload + }; + case ActionType.SET_USERS: + return { + ...state, + users: action.payload + }; + + case ActionType.SET_FILTER: + return { + ...state, + filter: action.payload && action.payload.filter + }; + + case ActionType.SET_ORDER_BY: + return { + ...state, + orderBy: action.payload && action.payload.orderBy + }; + + case ActionType.DELETE_USER: + const users = state.users.filter(user => user.id !== action.payload.id); + return { + ...state, + users + }; + + default: + return state; + } +} diff --git a/packages/server/.env b/packages/server/.env index e43508d..649b576 100644 --- a/packages/server/.env +++ b/packages/server/.env @@ -9,8 +9,8 @@ DB_SSL= # Auth AUTH_SECRET=secret, change for production -FACEBOOK_CLIENTID= -FACEBOOK_CLIENTSECRET= +FACEBOOK_CLIENTID=958659907661417 +FACEBOOK_CLIENTSECRET=ab666fb2b096fda0916d895e8d9f68c9 GITHUB_CLIENTID= GITHUB_CLIENTSECRET= LINKEDIN_CLIENTID= From 9a38a0dce5fabda79b0b553f838dedf7a8cd983b Mon Sep 17 00:00:00 2001 From: Ivan Isakov Date: Thu, 16 May 2019 12:13:10 +0300 Subject: [PATCH 066/104] Clear data --- packages/server/.env | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/.env b/packages/server/.env index 649b576..e43508d 100644 --- a/packages/server/.env +++ b/packages/server/.env @@ -9,8 +9,8 @@ DB_SSL= # Auth AUTH_SECRET=secret, change for production -FACEBOOK_CLIENTID=958659907661417 -FACEBOOK_CLIENTSECRET=ab666fb2b096fda0916d895e8d9f68c9 +FACEBOOK_CLIENTID= +FACEBOOK_CLIENTSECRET= GITHUB_CLIENTID= GITHUB_CLIENTSECRET= LINKEDIN_CLIENTID= From 886b611cf8c2e35cef5dc6ccaf6d7d8a7116c4d6 Mon Sep 17 00:00:00 2001 From: Ivan Isakov Date: Thu, 16 May 2019 16:09:54 +0300 Subject: [PATCH 067/104] Add request on currentUser in AuthRoute --- .../authentication/server-ts/access/jwt/index.ts | 13 +++++++++++-- modules/user/client-react/containers/Auth.tsx | 13 ++++++++----- modules/user/client-react/containers/AuthBase.tsx | 12 ++++++++---- .../containers/UserScreenNavigator.native.tsx | 2 +- modules/user/client-react/reducers/currentUser.ts | 2 +- modules/user/server-ts/index.ts | 2 +- modules/user/server-ts/locales/en/translations.json | 2 +- modules/user/server-ts/locales/ru/translations.json | 2 +- 8 files changed, 32 insertions(+), 16 deletions(-) diff --git a/modules/authentication/server-ts/access/jwt/index.ts b/modules/authentication/server-ts/access/jwt/index.ts index 7430eb7..6d3625e 100644 --- a/modules/authentication/server-ts/access/jwt/index.ts +++ b/modules/authentication/server-ts/access/jwt/index.ts @@ -1,4 +1,4 @@ -import { Express } from 'express'; +import { Express, Request, Response } from 'express'; import { Strategy as LocalStratery } from 'passport-local'; import passport from 'passport'; import { Strategy as JWTStrategy, ExtractJwt } from 'passport-jwt'; @@ -17,7 +17,16 @@ const beforeware = (app: Express) => { app.use(passport.initialize()); }; -const accessMiddleware = passport.authenticate('jwt', { session: false }); +const accessMiddleware = (req: Request, res: Response, next: any) => { + // passport.authenticate('jwt', { session: false }); + return req.isAuthenticated() + ? next() + : res.send({ + errors: { + message: 'unauthorized' + } + }); +}; const grant = async (identity: any, req: any, passwordHash: string = '') => { const refreshSecret = settings.auth.secret + passwordHash; diff --git a/modules/user/client-react/containers/Auth.tsx b/modules/user/client-react/containers/Auth.tsx index 5879d16..437c679 100644 --- a/modules/user/client-react/containers/Auth.tsx +++ b/modules/user/client-react/containers/Auth.tsx @@ -1,4 +1,4 @@ -import * as React from 'react'; +import React, { ComponentType, FunctionComponent } from 'react'; import { Route, Redirect, RouteComponentProps } from 'react-router-dom'; import { withLoadedUser } from './AuthBase'; import { UserRole, User } from '..'; @@ -8,12 +8,15 @@ interface AuthRouteProps extends WithUserProps { role?: UserRole | UserRole[]; redirect?: string; redirectOnLoggedIn?: boolean; - component?: React.ComponentType> | React.ComponentType; + component?: ComponentType> | ComponentType; } -const AuthRoute: React.ComponentType = withLoadedUser( - ({ currentUser, role, redirect = '/login', redirectOnLoggedIn, component: Component, ...rest }) => { - const RenderComponent: React.FunctionComponent = props => { +const AuthRoute: ComponentType = withLoadedUser( + ({ currentUser, role, redirect = '/login', redirectOnLoggedIn, component: Component, getCurrentUser, ...rest }) => { + const RenderComponent: FunctionComponent = props => { + if (currentUser === undefined) { + getCurrentUser(); + } // The users is not logged in if (redirectOnLoggedIn && currentUser) { return ; diff --git a/modules/user/client-react/containers/AuthBase.tsx b/modules/user/client-react/containers/AuthBase.tsx index 04ca876..e27881d 100644 --- a/modules/user/client-react/containers/AuthBase.tsx +++ b/modules/user/client-react/containers/AuthBase.tsx @@ -5,6 +5,7 @@ import { connect } from 'react-redux'; import authentication from '@restapp/authentication-client-react'; import { User, UserRole } from '..'; import CLEAR_USER from '../actions/clearUser'; +import { CURRENT_USER } from '../actions'; export interface WithUserProps extends RouteProps { currentUser?: User; @@ -30,10 +31,13 @@ export interface WithLogoutProps extends WithUserProps { const withUser = (Component: React.ComponentType) => { const WithUser = ({ currentUser, ...rest }: WithUserProps) => ; - return connect(({ currentUser: { loading, currentUser } }: any) => ({ - currentUserLoading: loading, - currentUser - }))(WithUser); + return connect( + ({ currentUser: { loading, currentUser } }: any) => ({ + currentUserLoading: loading, + currentUser + }), + { getCurrentUser: CURRENT_USER } + )(WithUser); }; const hasRole = (role: UserRole | UserRole[], currentUser: User) => { diff --git a/modules/user/client-react/containers/UserScreenNavigator.native.tsx b/modules/user/client-react/containers/UserScreenNavigator.native.tsx index 924ef4d..2c23e73 100644 --- a/modules/user/client-react/containers/UserScreenNavigator.native.tsx +++ b/modules/user/client-react/containers/UserScreenNavigator.native.tsx @@ -54,7 +54,7 @@ class UserScreenNavigator extends React.Component { }; public getInitialRoute = () => { const { currentUser } = this.props; - return currentUser ? 'Welcome' : 'Login'; + return currentUser ? 'Profile' : 'Login'; }; public render() { diff --git a/modules/user/client-react/reducers/currentUser.ts b/modules/user/client-react/reducers/currentUser.ts index 1909969..afe8862 100644 --- a/modules/user/client-react/reducers/currentUser.ts +++ b/modules/user/client-react/reducers/currentUser.ts @@ -13,7 +13,7 @@ export interface CurrentUserState { } const defaultState: CurrentUserState = { - currentUser: null, + currentUser: undefined, loading: false }; diff --git a/modules/user/server-ts/index.ts b/modules/user/server-ts/index.ts index d564afe..269b68a 100644 --- a/modules/user/server-ts/index.ts +++ b/modules/user/server-ts/index.ts @@ -25,7 +25,7 @@ const getHash = async (id: number) => ((await UserDAO.getUserWithPassword(id)) a const validateLogin = async (usernameOrEmail: string, pswd: string) => { const identity = (await UserDAO.getUserByUsernameOrEmail(usernameOrEmail)) as UserShape; - if (!identity) { + if (!identity || !identity.passwordHash) { return { message: i18n.t('user:auth.password.validPasswordEmail') }; } diff --git a/modules/user/server-ts/locales/en/translations.json b/modules/user/server-ts/locales/en/translations.json index a230977..d816ae1 100644 --- a/modules/user/server-ts/locales/en/translations.json +++ b/modules/user/server-ts/locales/en/translations.json @@ -8,7 +8,7 @@ "userCouldNotDeleted": "Could not delete user. Please try again later.", "auth": { "password": { - "validPasswordEmail": "Please enter a valid username or e-mail.", + "validPasswordEmail": "Please enter a valid username or password.", "emailConfirmation": "Please confirm your e-mail first.", "registrationFailed": "Registration failed due to validation errors", "validPassword": "Please enter a valid password.", diff --git a/modules/user/server-ts/locales/ru/translations.json b/modules/user/server-ts/locales/ru/translations.json index b50aa94..29227c8 100644 --- a/modules/user/server-ts/locales/ru/translations.json +++ b/modules/user/server-ts/locales/ru/translations.json @@ -8,7 +8,7 @@ "userCouldNotDeleted": "Невозможно удалить пользователя. Побробуйте повторить попытку позже.", "auth": { "password": { - "validPasswordEmail": "Пожалуйста, введите Ваш username или e-mail.", + "validPasswordEmail": "Пожалуйста, введите Ваш username или пароль.", "emailConfirmation": "Пожалуйста, подтвердите Ваш e-mail", "registrationFailed": "Регистрация не была успешной из-за ошибок валидации.", "validPassword": "Пожалуйста, введите действительный пароль.", From 30cb6269a255a615a3b15a8aed40cd84d54dcef5 Mon Sep 17 00:00:00 2001 From: Ivan Isakov Date: Thu, 16 May 2019 17:40:32 +0300 Subject: [PATCH 068/104] Implement working socils with jwt module --- .../server-ts/access/jwt/index.ts | 7 ++--- .../server-ts/access/session/index.ts | 2 +- modules/module/server-ts/ServerModule.ts | 6 ++--- modules/user/client-react/containers/Auth.tsx | 6 ++++- .../containers/DataRootComponent.native.tsx | 3 ++- .../user/client-react/containers/Login.tsx | 27 +++++++++++++++++-- modules/user/server-ts/social/shared.ts | 2 +- 7 files changed, 41 insertions(+), 12 deletions(-) diff --git a/modules/authentication/server-ts/access/jwt/index.ts b/modules/authentication/server-ts/access/jwt/index.ts index 6d3625e..abe0af8 100644 --- a/modules/authentication/server-ts/access/jwt/index.ts +++ b/modules/authentication/server-ts/access/jwt/index.ts @@ -17,8 +17,9 @@ const beforeware = (app: Express) => { app.use(passport.initialize()); }; -const accessMiddleware = (req: Request, res: Response, next: any) => { - // passport.authenticate('jwt', { session: false }); +const accessMiddleware = passport.authenticate('jwt', { session: false }); + +const checkAuthentication = (req: Request, res: Response, next: any) => { return req.isAuthenticated() ? next() : res.send({ @@ -103,6 +104,6 @@ export default (settings.auth.jwt.enabled controller: refreshTokens } ], - accessMiddleware + accessMiddleware: [accessMiddleware, checkAuthentication] }) : undefined); diff --git a/modules/authentication/server-ts/access/session/index.ts b/modules/authentication/server-ts/access/session/index.ts index a1a226c..b6b5c30 100644 --- a/modules/authentication/server-ts/access/session/index.ts +++ b/modules/authentication/server-ts/access/session/index.ts @@ -88,7 +88,7 @@ export default (settings.auth.session.enabled ? new AccessModule({ beforeware: [beforeware], onAppCreate: [onAppCreate], - accessMiddleware, + accessMiddleware: [accessMiddleware], appContext: sessionAppContext, apiRouteParams: [ { diff --git a/modules/module/server-ts/ServerModule.ts b/modules/module/server-ts/ServerModule.ts index 4592690..9dd0472 100644 --- a/modules/module/server-ts/ServerModule.ts +++ b/modules/module/server-ts/ServerModule.ts @@ -57,7 +57,7 @@ export interface ServerModuleShape extends CommonModuleShape { beforeware?: MiddlewareFunc[]; // A list of functions to register normal-priority middlewares middleware?: MiddlewareFunc[]; - accessMiddleware?: RequestHandler; + accessMiddleware?: RequestHandler[]; apiRouteParams?: Array<{ method: RestMethod; route: string; @@ -103,8 +103,8 @@ class ServerModule extends CommonModule { return (app: Express, modules: ServerModule) => { const handlers = []; - if (isAuthRoute && modules.accessMiddleware) { - handlers.push(modules.accessMiddleware); + if (isAuthRoute && modules.accessMiddleware && modules.accessMiddleware.length) { + handlers.push(...modules.accessMiddleware); } if (!isEmpty(middleware)) { handlers.push(...middleware); diff --git a/modules/user/client-react/containers/Auth.tsx b/modules/user/client-react/containers/Auth.tsx index 437c679..cb58304 100644 --- a/modules/user/client-react/containers/Auth.tsx +++ b/modules/user/client-react/containers/Auth.tsx @@ -15,7 +15,11 @@ const AuthRoute: ComponentType = withLoadedUser( ({ currentUser, role, redirect = '/login', redirectOnLoggedIn, component: Component, getCurrentUser, ...rest }) => { const RenderComponent: FunctionComponent = props => { if (currentUser === undefined) { - getCurrentUser(); + (async () => { + try { + await getCurrentUser(); + } catch (e) {} + })(); } // The users is not logged in if (redirectOnLoggedIn && currentUser) { diff --git a/modules/user/client-react/containers/DataRootComponent.native.tsx b/modules/user/client-react/containers/DataRootComponent.native.tsx index d760074..0b65fc1 100644 --- a/modules/user/client-react/containers/DataRootComponent.native.tsx +++ b/modules/user/client-react/containers/DataRootComponent.native.tsx @@ -5,6 +5,7 @@ import { getItem } from '@restapp/core-common/clientStorage'; import Loading from '../components/Loading.native'; import { CURRENT_USER } from '../actions'; import { User } from '..'; +import setting from '../../../../settings'; interface DataRootComponent { currentUser: User; @@ -18,7 +19,7 @@ class DataRootComponent extends React.Component { public async componentDidMount() { const { currentUser, getCurrentUser } = this.props; - if (!this.state.ready && (await getItem('refreshToken')) && !currentUser) { + if (!this.state.ready && !currentUser && ((await getItem('refreshToken')) || setting.auth.session.enabled)) { await getCurrentUser(); } this.setState({ ready: true }); diff --git a/modules/user/client-react/containers/Login.tsx b/modules/user/client-react/containers/Login.tsx index e9b5c5e..e8315cc 100644 --- a/modules/user/client-react/containers/Login.tsx +++ b/modules/user/client-react/containers/Login.tsx @@ -1,8 +1,9 @@ -import * as React from 'react'; +import React, { useEffect } from 'react'; import { connect } from 'react-redux'; import { translate } from '@restapp/i18n-client-react'; import authentication from '@restapp/authentication-client-react'; import { FormError } from '@restapp/forms-client-react'; +import { setItem } from '@restapp/core-common/clientStorage'; import LoginView from '../components/LoginView'; import { CommonProps, LoginSubmitProps } from '..'; @@ -22,13 +23,35 @@ const Login: React.FunctionComponent = props => { const [isRegistered, setIsRegistered] = React.useState(false); const [isReady, setIsReady] = React.useState(false); - React.useEffect(() => { + useEffect(() => { + if (search && search.includes('data')) { + checkAndSaveTokens(); + } + }, []); + + useEffect(() => { if (search.includes('email-verified')) { setIsRegistered(true); } setIsReady(true); }, []); + const checkAndSaveTokens = async () => { + const dataRegExp = /data=([^#]+)/; + + const [, data] = search.match(dataRegExp); + const decodedData = JSON.parse(decodeURI(data)); + + if (decodedData.tokens) { + await setItem('accessToken', decodedData.tokens.accessToken); + await setItem('refreshToken', decodedData.tokens.refreshToken); + + await authentication.doLogin(); + } + + history.push('profile'); + }; + const hideModal = () => { setIsRegistered(false); history.push({ search: '' }); diff --git a/modules/user/server-ts/social/shared.ts b/modules/user/server-ts/social/shared.ts index 344d568..ea80be7 100644 --- a/modules/user/server-ts/social/shared.ts +++ b/modules/user/server-ts/social/shared.ts @@ -15,7 +15,7 @@ export async function onAuthenticationSuccess(req: any, res: any) { if (redirectUrl) { res.redirect(redirectUrl + (tokens ? '?data=' + JSON.stringify({ tokens }) : '')); } else { - res.redirect('/profile'); + res.redirect(`/login${tokens ? '?data=' + JSON.stringify({ tokens }) : ''}`); } } From d227a1ed3e259a71b93be2fcad484fb3bb75ab4c Mon Sep 17 00:00:00 2001 From: Ivan Isakov Date: Fri, 17 May 2019 18:03:08 +0300 Subject: [PATCH 069/104] Fix social auth with jwt --- .../client-react/access/jwt/index.ts | 11 ++++--- .../server-ts/access/jwt/controllers.ts | 2 +- .../server-ts/access/jwt/index.ts | 33 ++++++++++++------- .../server-ts/access/session/index.ts | 1 + modules/core/common/createReduxStore.ts | 6 ++-- modules/user/client-react/actions/addUser.ts | 3 +- .../user/client-react/actions/currentUser.ts | 6 +++- .../user/client-react/actions/deleteUser.ts | 4 ++- modules/user/client-react/actions/editUser.ts | 3 +- .../client-react/actions/forgotPassword.ts | 3 +- modules/user/client-react/actions/login.ts | 5 ++- modules/user/client-react/actions/register.ts | 2 +- .../client-react/actions/resetPassword.ts | 3 +- modules/user/client-react/actions/user.ts | 4 ++- modules/user/client-react/actions/users.ts | 6 +++- modules/user/client-react/containers/Auth.tsx | 14 +++----- .../user/client-react/containers/AuthBase.tsx | 33 +++++++++++-------- .../user/client-react/containers/Profile.tsx | 12 +++---- modules/user/client-react/index.tsx | 4 +-- modules/user/client-react/reducers/index.ts | 1 + modules/user/client-react/reducers/users.ts | 7 ++++ 21 files changed, 97 insertions(+), 66 deletions(-) diff --git a/modules/authentication/client-react/access/jwt/index.ts b/modules/authentication/client-react/access/jwt/index.ts index e76810a..cc76fa9 100644 --- a/modules/authentication/client-react/access/jwt/index.ts +++ b/modules/authentication/client-react/access/jwt/index.ts @@ -42,13 +42,16 @@ const refreshAccessToken = async () => { const reduxMiddleware: Middleware = ({ dispatch }) => next => action => { const { types, status, ...rest } = action; - (async () => { try { if (status === 401) { - await refreshAccessToken(); - const newAction = { ...action, status: null }; - return dispatch(newAction); + try { + await refreshAccessToken(); + const newAction = { ...action, status: null }; + return dispatch(newAction); + } catch (e) { + throw e; + } } return next(action); } catch (e) { diff --git a/modules/authentication/server-ts/access/jwt/controllers.ts b/modules/authentication/server-ts/access/jwt/controllers.ts index 004853c..783b3b4 100644 --- a/modules/authentication/server-ts/access/jwt/controllers.ts +++ b/modules/authentication/server-ts/access/jwt/controllers.ts @@ -33,7 +33,7 @@ export const refreshTokens = async (req: any, res: any) => { try { jwt.verify(inputRefreshToken, refreshSecret); } catch (err) { - return res.send(err); + return res.status(401).send({ errors: err }); } const [accessToken, refreshToken] = await createTokens(identity, settings.auth.secret, refreshSecret, req.t); diff --git a/modules/authentication/server-ts/access/jwt/index.ts b/modules/authentication/server-ts/access/jwt/index.ts index abe0af8..6fab715 100644 --- a/modules/authentication/server-ts/access/jwt/index.ts +++ b/modules/authentication/server-ts/access/jwt/index.ts @@ -1,4 +1,4 @@ -import { Express, Request, Response } from 'express'; +import { Express, Request, Response, NextFunction } from 'express'; import { Strategy as LocalStratery } from 'passport-local'; import passport from 'passport'; import { Strategy as JWTStrategy, ExtractJwt } from 'passport-jwt'; @@ -17,20 +17,31 @@ const beforeware = (app: Express) => { app.use(passport.initialize()); }; -const accessMiddleware = passport.authenticate('jwt', { session: false }); +const accessMiddleware = (req: Request, res: Response, next: NextFunction) => + passport.authenticate('jwt', { session: false }, (_err, user, info) => { + if (info) { + res.locals.error = info; + return next(); + } + req.user = { ...user }; + return next(); + })(req, res, next); -const checkAuthentication = (req: Request, res: Response, next: any) => { - return req.isAuthenticated() - ? next() - : res.send({ - errors: { - message: 'unauthorized' - } - }); +const checkAuthentication = (req: Request, res: Response, next: NextFunction) => { + if (req.user) { + return next(); + } + return res.send({ + status: 401, + errors: { + message: res.locals.error ? res.locals.error.message : 'unauthorized' + } + }); }; const grant = async (identity: any, req: any, passwordHash: string = '') => { - const refreshSecret = settings.auth.secret + passwordHash; + const refreshSecret = settings.auth.secret + (passwordHash || ''); + const [accessToken, refreshToken] = await createTokens(identity, settings.auth.secret, refreshSecret, req.t); return { diff --git a/modules/authentication/server-ts/access/session/index.ts b/modules/authentication/server-ts/access/session/index.ts index b6b5c30..d2b53aa 100644 --- a/modules/authentication/server-ts/access/session/index.ts +++ b/modules/authentication/server-ts/access/session/index.ts @@ -30,6 +30,7 @@ const accessMiddleware = (req: Request, res: Response, next: any) => req.isAuthenticated() ? next() : res.send({ + status: 401, errors: { message: 'unauthorized' } diff --git a/modules/core/common/createReduxStore.ts b/modules/core/common/createReduxStore.ts index 36b96bd..4bd4e54 100644 --- a/modules/core/common/createReduxStore.ts +++ b/modules/core/common/createReduxStore.ts @@ -15,7 +15,7 @@ const requestMiddleware: Middleware = _state => next => action => { return next(action); } - const [REQUEST, SUCCESS, FAIL] = types; + const { REQUEST, SUCCESS, FAIL } = types; next({ type: REQUEST, ...rest }); @@ -33,8 +33,8 @@ const requestMiddleware: Middleware = _state => next => action => { }); return data; } catch (e) { - if (e.response && e.response.status === 401) { - return next({ ...action, status: e.response.status }); + if (e.response && e.response.data && e.response.data.status === 401) { + return next({ ...action, status: e.response.data.status }); } const data = e.response && e.response.data; next({ diff --git a/modules/user/client-react/actions/addUser.ts b/modules/user/client-react/actions/addUser.ts index c8e95b7..53ce677 100644 --- a/modules/user/client-react/actions/addUser.ts +++ b/modules/user/client-react/actions/addUser.ts @@ -1,10 +1,9 @@ import axios from 'axios'; -import { ActionType } from '../reducers'; import { User } from '..'; export default function ADD_USER(user: User) { return { - types: [null, null, null] as ActionType[], + types: {}, APICall: () => axios.post(`${__API_URL__}/addUser`, { ...user }) }; } diff --git a/modules/user/client-react/actions/currentUser.ts b/modules/user/client-react/actions/currentUser.ts index 34bd4e5..97aac0c 100644 --- a/modules/user/client-react/actions/currentUser.ts +++ b/modules/user/client-react/actions/currentUser.ts @@ -3,7 +3,11 @@ import { ActionType } from '../reducers'; export default function CURRENT_USER() { return { - types: [null, ActionType.SET_CURRENT_USER, ActionType.CLEAR_CURRENT_USER], + types: { + REQUEST: ActionType.CLEAR_CURRENT_USER, + SUCCESS: ActionType.SET_CURRENT_USER, + FAIL: ActionType.CLEAR_CURRENT_USER + }, APICall: () => axios.get(`${__API_URL__}/currentUser`) }; } diff --git a/modules/user/client-react/actions/deleteUser.ts b/modules/user/client-react/actions/deleteUser.ts index 19e69f6..ed55d61 100644 --- a/modules/user/client-react/actions/deleteUser.ts +++ b/modules/user/client-react/actions/deleteUser.ts @@ -3,7 +3,9 @@ import { ActionType } from '../reducers'; export default function DELETE_USER(id: number) { return { - types: [null, ActionType.DELETE_USER, null] as ActionType[], + types: { + SUCCESS: ActionType.DELETE_USER + }, APICall: () => axios.delete(`${__API_URL__}/deleteUser`, { data: { id } }) }; } diff --git a/modules/user/client-react/actions/editUser.ts b/modules/user/client-react/actions/editUser.ts index d8818e4..fd4a609 100644 --- a/modules/user/client-react/actions/editUser.ts +++ b/modules/user/client-react/actions/editUser.ts @@ -1,10 +1,9 @@ import axios from 'axios'; -import { ActionType } from '../reducers'; import { User } from '..'; export default function EDIT_USER(user: User) { return { - types: [null, null, null] as ActionType[], + types: {}, APICall: () => axios.post(`${__API_URL__}/editUser`, { ...user }) }; } diff --git a/modules/user/client-react/actions/forgotPassword.ts b/modules/user/client-react/actions/forgotPassword.ts index 88104ea..7f5aaf5 100644 --- a/modules/user/client-react/actions/forgotPassword.ts +++ b/modules/user/client-react/actions/forgotPassword.ts @@ -1,10 +1,9 @@ import axios from 'axios'; import { ForgotPasswordSubmitProps } from '..'; -import { ActionType } from '../reducers'; export default function FORGOT_PASSWORD(value: ForgotPasswordSubmitProps) { return { - types: [null, null, null] as ActionType[], + types: {}, APICall: () => axios.post(`${__API_URL__}/forgotPassword`, { value }) }; } diff --git a/modules/user/client-react/actions/login.ts b/modules/user/client-react/actions/login.ts index cbaa74f..7e34347 100644 --- a/modules/user/client-react/actions/login.ts +++ b/modules/user/client-react/actions/login.ts @@ -4,7 +4,10 @@ import { ActionType } from '../reducers'; export default function LOGIN(value: LoginSubmitProps) { return { - types: [null, ActionType.SET_CURRENT_USER, ActionType.CLEAR_CURRENT_USER], + types: { + SUCCESS: ActionType.SET_CURRENT_USER, + FAIL: ActionType.CLEAR_CURRENT_USER + }, APICall: () => axios.post(`${__API_URL__}/login`, { ...value }) }; } diff --git a/modules/user/client-react/actions/register.ts b/modules/user/client-react/actions/register.ts index 5a2686e..b976900 100644 --- a/modules/user/client-react/actions/register.ts +++ b/modules/user/client-react/actions/register.ts @@ -3,7 +3,7 @@ import { RegisterSubmitProps } from '..'; export default function REGISTER(value: RegisterSubmitProps) { return { - types: [null, null, null] as any, + types: {}, APICall: () => axios.post(`${__API_URL__}/register`, { ...value }) }; } diff --git a/modules/user/client-react/actions/resetPassword.ts b/modules/user/client-react/actions/resetPassword.ts index ad5982c..5546002 100644 --- a/modules/user/client-react/actions/resetPassword.ts +++ b/modules/user/client-react/actions/resetPassword.ts @@ -1,6 +1,5 @@ import axios from 'axios'; import { ResetPasswordSubmitProps } from '..'; -import { ActionType } from '../reducers'; interface ResetPasswordProps extends ResetPasswordSubmitProps { token: string; @@ -8,7 +7,7 @@ interface ResetPasswordProps extends ResetPasswordSubmitProps { export default function RESET_PASSWORD(value: ResetPasswordProps) { return { - types: [null, null, null] as ActionType[], + types: {}, APICall: () => axios.post(`${__API_URL__}/resetPassword`, { ...value }) }; } diff --git a/modules/user/client-react/actions/user.ts b/modules/user/client-react/actions/user.ts index 3047bdf..41d5ca9 100644 --- a/modules/user/client-react/actions/user.ts +++ b/modules/user/client-react/actions/user.ts @@ -3,7 +3,9 @@ import { ActionType } from '../reducers'; export default function USER(id: number) { return { - types: [null, ActionType.SET_USER, null], + types: { + SUCCESS: ActionType.SET_USER + }, APICall: () => axios.get(`${__API_URL__}/user/${id}`) }; } diff --git a/modules/user/client-react/actions/users.ts b/modules/user/client-react/actions/users.ts index 8c7c851..9369ba8 100644 --- a/modules/user/client-react/actions/users.ts +++ b/modules/user/client-react/actions/users.ts @@ -4,7 +4,11 @@ import { OrderBy, Filter } from '..'; export default function USERS(orderBy: OrderBy, filter: Filter, type?: ActionType) { return { - types: [type ? ActionType[type] : null, ActionType.SET_USERS, null], + types: { + REQUEST: type ? ActionType[type] : null, + SUCCESS: ActionType.SET_USERS, + FAIL: ActionType.CLEAR_USERS + }, payload: { orderBy, filter }, APICall: () => axios.post(`${__API_URL__}/users`, { filter, orderBy }) }; diff --git a/modules/user/client-react/containers/Auth.tsx b/modules/user/client-react/containers/Auth.tsx index cb58304..c239ead 100644 --- a/modules/user/client-react/containers/Auth.tsx +++ b/modules/user/client-react/containers/Auth.tsx @@ -1,6 +1,7 @@ import React, { ComponentType, FunctionComponent } from 'react'; import { Route, Redirect, RouteComponentProps } from 'react-router-dom'; -import { withLoadedUser } from './AuthBase'; + +import { withUser } from './AuthBase'; import { UserRole, User } from '..'; import { WithUserProps } from './AuthBase'; @@ -11,16 +12,9 @@ interface AuthRouteProps extends WithUserProps { component?: ComponentType> | ComponentType; } -const AuthRoute: ComponentType = withLoadedUser( - ({ currentUser, role, redirect = '/login', redirectOnLoggedIn, component: Component, getCurrentUser, ...rest }) => { +const AuthRoute: ComponentType = withUser( + ({ currentUser, role, redirect = '/login', redirectOnLoggedIn, component: Component, ...rest }) => { const RenderComponent: FunctionComponent = props => { - if (currentUser === undefined) { - (async () => { - try { - await getCurrentUser(); - } catch (e) {} - })(); - } // The users is not logged in if (redirectOnLoggedIn && currentUser) { return ; diff --git a/modules/user/client-react/containers/AuthBase.tsx b/modules/user/client-react/containers/AuthBase.tsx index e27881d..7c6786e 100644 --- a/modules/user/client-react/containers/AuthBase.tsx +++ b/modules/user/client-react/containers/AuthBase.tsx @@ -1,12 +1,15 @@ -import * as React from 'react'; +import React, { useEffect } from 'react'; import { RouteProps } from 'react-router'; import { History } from 'history'; import { connect } from 'react-redux'; import authentication from '@restapp/authentication-client-react'; +import { getItem } from '@restapp/core-common/clientStorage'; import { User, UserRole } from '..'; import CLEAR_USER from '../actions/clearUser'; import { CURRENT_USER } from '../actions'; +import setting from '../../../../settings'; + export interface WithUserProps extends RouteProps { currentUser?: User; currentUserLoading?: boolean; @@ -30,7 +33,19 @@ export interface WithLogoutProps extends WithUserProps { } const withUser = (Component: React.ComponentType) => { - const WithUser = ({ currentUser, ...rest }: WithUserProps) => ; + const WithUser = ({ currentUser, getCurrentUser, currentUserLoading, ...rest }: WithUserProps) => { + useEffect(() => { + (async () => { + if (currentUser === undefined && ((await getItem('refreshToken')) || setting.auth.session.enabled)) { + try { + await getCurrentUser(); + } catch (e) {} + } + })(); + }, []); + + return currentUserLoading ? null : ; + }; return connect( ({ currentUser: { loading, currentUser } }: any) => ({ currentUserLoading: loading, @@ -44,14 +59,6 @@ const hasRole = (role: UserRole | UserRole[], currentUser: User) => { return currentUser && (!role || (Array.isArray(role) ? role : [role]).indexOf(currentUser.role) >= 0) ? true : false; }; -const withLoadedUser = (Component: React.ComponentType) => { - const WithLoadedUser = ({ currentUserLoading, ...props }: WithUserProps) => { - return currentUserLoading ? null : ; - }; - - return withUser(WithLoadedUser); -}; - const IfLoggedInComponent: React.FunctionComponent = ({ currentUser, role, @@ -59,13 +66,13 @@ const IfLoggedInComponent: React.FunctionComponent = ({ elseComponent }) => (hasRole(role, currentUser) ? React.cloneElement(children, {}) : elseComponent || null); -const IfLoggedIn: React.ComponentType = withLoadedUser(IfLoggedInComponent); +const IfLoggedIn: React.ComponentType = withUser(IfLoggedInComponent); const IfNotLoggedInComponent: React.FunctionComponent = ({ currentUser, children }) => { return !currentUser ? React.cloneElement(children, {}) : null; }; -const IfNotLoggedIn: React.ComponentType = withLoadedUser(IfNotLoggedInComponent); +const IfNotLoggedIn: React.ComponentType = withUser(IfNotLoggedInComponent); const withLogout = (Component: React.ComponentType) => { const WithLogout = ({ clearUser, ...props }: WithLogoutProps) => { @@ -82,4 +89,4 @@ const withLogout = (Component: React.ComponentType) => { )(WithLogout); }; -export { withUser, hasRole, withLoadedUser, IfLoggedIn, IfNotLoggedIn, withLogout }; +export { withUser, hasRole, IfLoggedIn, IfNotLoggedIn, withLogout }; diff --git a/modules/user/client-react/containers/Profile.tsx b/modules/user/client-react/containers/Profile.tsx index 48d6e19..8d5558f 100644 --- a/modules/user/client-react/containers/Profile.tsx +++ b/modules/user/client-react/containers/Profile.tsx @@ -2,7 +2,6 @@ import React from 'react'; import { connect } from 'react-redux'; import ProfileView from '../components/ProfileView'; import { User } from '..'; -import { CURRENT_USER } from '../actions'; interface ProfileProps { currentUser: User; @@ -10,13 +9,10 @@ interface ProfileProps { getCurrentUser: () => void; } -const Profile: React.FunctionComponent = ({ getCurrentUser, ...props }) => { +const Profile: React.FunctionComponent = ({ ...props }) => { return ; }; -export default connect( - ({ currentUser: { currentUser } }: any) => ({ - currentUser - }), - { getCurrentUser: CURRENT_USER } -)(Profile); +export default connect(({ currentUser: { currentUser } }: any) => ({ + currentUser +}))(Profile); diff --git a/modules/user/client-react/index.tsx b/modules/user/client-react/index.tsx index cd6b571..f5c0a1a 100644 --- a/modules/user/client-react/index.tsx +++ b/modules/user/client-react/index.tsx @@ -19,7 +19,7 @@ import ForgotPassword from './containers/ForgotPassword'; import ResetPassword from './containers/ResetPassword'; import { currentUserReducer, usersReducer } from './reducers'; -import { AuthRoute, IfLoggedIn, IfNotLoggedIn, withLoadedUser, withLogout, WithLogoutProps } from './containers/Auth'; +import { AuthRoute, IfLoggedIn, IfNotLoggedIn, withLogout, WithLogoutProps, withUser } from './containers/Auth'; export enum UserRole { admin = 'admin', @@ -103,7 +103,7 @@ export interface Filter { isActive: boolean; } -const ProfileName = withLoadedUser(({ currentUser }) => ( +const ProfileName = withUser(({ currentUser }) => ( <>{currentUser ? currentUser.fullName || currentUser.username : null} )); diff --git a/modules/user/client-react/reducers/index.ts b/modules/user/client-react/reducers/index.ts index 7e17c9c..d41fd99 100644 --- a/modules/user/client-react/reducers/index.ts +++ b/modules/user/client-react/reducers/index.ts @@ -7,6 +7,7 @@ export enum ActionType { SET_LOADING = 'SET_LOADING', SET_USER = 'SET_USER', SET_USERS = 'SET_USERS', + CLEAR_USERS = 'CLEAR_USERS', SET_ORDER_BY = 'SET_ORDER_BY', SET_FILTER = 'SET_FILTER', DELETE_USER = 'DELETE_USER' diff --git a/modules/user/client-react/reducers/users.ts b/modules/user/client-react/reducers/users.ts index 88ea9ba..b0d8ac8 100644 --- a/modules/user/client-react/reducers/users.ts +++ b/modules/user/client-react/reducers/users.ts @@ -30,12 +30,19 @@ export default function(state = defaultState, action: UserModuleActionProps) { ...state, user: action.payload }; + case ActionType.SET_USERS: return { ...state, users: action.payload }; + case ActionType.CLEAR_USERS: + return { + ...state, + users: null + }; + case ActionType.SET_FILTER: return { ...state, From 7cb9828d70c14c52e535276203dc896f5c2d611a Mon Sep 17 00:00:00 2001 From: Ivan Isakov Date: Mon, 20 May 2019 11:07:01 +0300 Subject: [PATCH 070/104] Fix socials with session --- modules/authentication/server-ts/access/AccessModule.ts | 8 +++++--- modules/core/common/createReduxStore.ts | 6 +++--- modules/user/client-react/containers/Login.tsx | 1 + packages/server/.env | 4 ++-- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/modules/authentication/server-ts/access/AccessModule.ts b/modules/authentication/server-ts/access/AccessModule.ts index 1a01662..928f961 100644 --- a/modules/authentication/server-ts/access/AccessModule.ts +++ b/modules/authentication/server-ts/access/AccessModule.ts @@ -5,7 +5,6 @@ import ServerModule, { ServerModuleShape } from '@restapp/module-server-ts'; interface AccessModuleShape extends ServerModuleShape { grant?: Array<(identity: UserShape, req: Request, passwordHash: string) => { [key: string]: any } | void>; - // grant?: (user: any) => Promise<[string, string]>; } interface AccessModule extends AccessModuleShape {} @@ -20,9 +19,12 @@ class AccessModule extends ServerModule { get grantAccess(): GrantAccessFunc { return async (identity: UserShape, req: Request, passwordHash: string) => { let result = {}; - for (const grant of this.grant) { - result = merge(result, await grant(identity, req, passwordHash)); + if (this.grant) { + for (const grant of this.grant) { + result = merge(result, await grant(identity, req, passwordHash)); + } } + return result; }; } diff --git a/modules/core/common/createReduxStore.ts b/modules/core/common/createReduxStore.ts index 4bd4e54..e30cc55 100644 --- a/modules/core/common/createReduxStore.ts +++ b/modules/core/common/createReduxStore.ts @@ -17,7 +17,7 @@ const requestMiddleware: Middleware = _state => next => action => { const { REQUEST, SUCCESS, FAIL } = types; - next({ type: REQUEST, ...rest }); + next({ type: REQUEST || null, ...rest }); const handleAPICall = async () => { try { @@ -27,7 +27,7 @@ const requestMiddleware: Middleware = _state => next => action => { throw { response: result }; } next({ - type: SUCCESS, + type: SUCCESS || null, ...rest, payload: data }); @@ -38,7 +38,7 @@ const requestMiddleware: Middleware = _state => next => action => { } const data = e.response && e.response.data; next({ - type: FAIL, + type: FAIL || null, ...rest, payload: data }); diff --git a/modules/user/client-react/containers/Login.tsx b/modules/user/client-react/containers/Login.tsx index e8315cc..9355e77 100644 --- a/modules/user/client-react/containers/Login.tsx +++ b/modules/user/client-react/containers/Login.tsx @@ -62,6 +62,7 @@ const Login: React.FunctionComponent = props => { await login(values); } catch (e) { const data = e.response && e.response.data; + throw new FormError(t('reg.errorMsg'), data); } diff --git a/packages/server/.env b/packages/server/.env index e43508d..649b576 100644 --- a/packages/server/.env +++ b/packages/server/.env @@ -9,8 +9,8 @@ DB_SSL= # Auth AUTH_SECRET=secret, change for production -FACEBOOK_CLIENTID= -FACEBOOK_CLIENTSECRET= +FACEBOOK_CLIENTID=958659907661417 +FACEBOOK_CLIENTSECRET=ab666fb2b096fda0916d895e8d9f68c9 GITHUB_CLIENTID= GITHUB_CLIENTSECRET= LINKEDIN_CLIENTID= From dd6a25cad47481f365f90d287c58de3c4d8fa12d Mon Sep 17 00:00:00 2001 From: Ivan Isakov Date: Mon, 20 May 2019 11:14:56 +0300 Subject: [PATCH 071/104] Clear data --- config/auth.js | 2 +- modules/user/server-ts/password/controllers.ts | 2 +- packages/server/.env | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/config/auth.js b/config/auth.js index 11f648a..a770655 100644 --- a/config/auth.js +++ b/config/auth.js @@ -16,7 +16,7 @@ export default { }, social: { facebook: { - enabled: true, + enabled: false, clientID: process.env.FACEBOOK_CLIENTID, clientSecret: process.env.FACEBOOK_CLIENTSECRET, callbackURL: '/api/auth/facebook/callback', diff --git a/modules/user/server-ts/password/controllers.ts b/modules/user/server-ts/password/controllers.ts index 27405c1..8660234 100644 --- a/modules/user/server-ts/password/controllers.ts +++ b/modules/user/server-ts/password/controllers.ts @@ -11,7 +11,7 @@ import emailTemplate from '../emailTemplate'; import { createPasswordHash } from '.'; const { - auth: { passwordSettings, secret }, + auth: { password: passwordSettings, secret }, app } = settings; diff --git a/packages/server/.env b/packages/server/.env index 649b576..e43508d 100644 --- a/packages/server/.env +++ b/packages/server/.env @@ -9,8 +9,8 @@ DB_SSL= # Auth AUTH_SECRET=secret, change for production -FACEBOOK_CLIENTID=958659907661417 -FACEBOOK_CLIENTSECRET=ab666fb2b096fda0916d895e8d9f68c9 +FACEBOOK_CLIENTID= +FACEBOOK_CLIENTSECRET= GITHUB_CLIENTID= GITHUB_CLIENTSECRET= LINKEDIN_CLIENTID= From 8e91567c01690f961da1ebb19fe5539f01156310 Mon Sep 17 00:00:00 2001 From: Ivan Isakov Date: Mon, 20 May 2019 11:49:43 +0300 Subject: [PATCH 072/104] Change interfaces --- .../authentication/server-ts/access/index.ts | 10 +++--- .../server-ts/access/jwt/index.ts | 4 +-- modules/user/server-ts/controllers.ts | 2 -- modules/user/server-ts/index.ts | 7 ++-- .../user/server-ts/password/controllers.ts | 8 ++--- modules/user/server-ts/social/shared.ts | 4 +-- modules/user/server-ts/sql.ts | 34 +++++++++---------- 7 files changed, 33 insertions(+), 36 deletions(-) diff --git a/modules/authentication/server-ts/access/index.ts b/modules/authentication/server-ts/access/index.ts index e0b9b14..db2e9a8 100644 --- a/modules/authentication/server-ts/access/index.ts +++ b/modules/authentication/server-ts/access/index.ts @@ -5,12 +5,12 @@ import resources from '../locales'; import AccessModule from './AccessModule'; export interface UserShape { - id: number; + id?: number; username: string; - role: string; - isActive: boolean; - email: string; - passwordHash: string; + role?: string; + isActive?: boolean; + email?: string; + passwordHash?: string; } // Try to grant access via sessions first, and if that fails, then try using JWT diff --git a/modules/authentication/server-ts/access/jwt/index.ts b/modules/authentication/server-ts/access/jwt/index.ts index 6fab715..df6918a 100644 --- a/modules/authentication/server-ts/access/jwt/index.ts +++ b/modules/authentication/server-ts/access/jwt/index.ts @@ -1,5 +1,5 @@ import { Express, Request, Response, NextFunction } from 'express'; -import { Strategy as LocalStratery } from 'passport-local'; +import { Strategy as LocalStrategy } from 'passport-local'; import passport from 'passport'; import { Strategy as JWTStrategy, ExtractJwt } from 'passport-jwt'; @@ -73,7 +73,7 @@ const loginMiddleware = (req: any, res: any, next: any) => { const onAppCreate = ({ appContext }: AccessModule) => { passport.use( - new LocalStratery({ usernameField: 'usernameOrEmail' }, async (username: string, password: string, done: any) => { + new LocalStrategy({ usernameField: 'usernameOrEmail' }, async (username: string, password: string, done: any) => { const { user, message } = await appContext.user.validateLogin(username, password); if (message) { diff --git a/modules/user/server-ts/controllers.ts b/modules/user/server-ts/controllers.ts index 290e247..7e9a03a 100644 --- a/modules/user/server-ts/controllers.ts +++ b/modules/user/server-ts/controllers.ts @@ -152,13 +152,11 @@ export const editUser = async ({ user: identity, body, t }: any, res: any) => { const userInfo = !isSelf() && isAdmin() ? body : pick(body, ['id', 'username', 'email', 'password']); - const isProfileExists = await userDAO.isUserProfileExists(body.id); const passwordHash = await createPasswordHash(body.password); const trx = await createTransaction(); try { await userDAO.editUser(userInfo, passwordHash).transacting(trx); - await userDAO.editUserProfile(body, isProfileExists).transacting(trx); if (mailer && body.password && password.sendPasswordChangesEmail) { const url = `${__WEBSITE_URL__}/profile`; diff --git a/modules/user/server-ts/index.ts b/modules/user/server-ts/index.ts index 269b68a..47d38c4 100644 --- a/modules/user/server-ts/index.ts +++ b/modules/user/server-ts/index.ts @@ -6,7 +6,7 @@ import ServerModule, { RestMethod } from '@restapp/module-server-ts'; import { user, users, currentUser, addUser, editUser, deleteUser } from './controllers'; import password from './password'; import social from './social'; -import UserDAO, { UserShape } from './sql'; +import UserDAO, { UserShape, UserShapePassword } from './sql'; import settings from '../../../settings'; import resources from './locales'; @@ -20,10 +20,11 @@ const getIdentity = (id: number) => { return UserDAO.getUser(id); }; -const getHash = async (id: number) => ((await UserDAO.getUserWithPassword(id)) as UserShape).passwordHash || ''; +const getHash = async (id: number) => + ((await UserDAO.getUserWithPassword(id)) as UserShape & UserShapePassword).passwordHash || ''; const validateLogin = async (usernameOrEmail: string, pswd: string) => { - const identity = (await UserDAO.getUserByUsernameOrEmail(usernameOrEmail)) as UserShape; + const identity = (await UserDAO.getUserByUsernameOrEmail(usernameOrEmail)) as UserShape & UserShapePassword; if (!identity || !identity.passwordHash) { return { message: i18n.t('user:auth.password.validPasswordEmail') }; diff --git a/modules/user/server-ts/password/controllers.ts b/modules/user/server-ts/password/controllers.ts index 8660234..d0bd1eb 100644 --- a/modules/user/server-ts/password/controllers.ts +++ b/modules/user/server-ts/password/controllers.ts @@ -1,4 +1,4 @@ -import { UserShape } from './../sql'; +import { UserShape, UserShapePassword } from './../sql'; import { pick, isEmpty } from 'lodash'; import jwt from 'jsonwebtoken'; import { log } from '@restapp/core-common'; @@ -82,7 +82,7 @@ export const register = async ({ body, t }: any, res: any) => { export const forgotPassword = async ({ body, t }: any, res: any) => { try { const localAuth = pick(body, 'email'); - const identity = (await userDAO.getUserByEmail(localAuth.email)) as UserShape; + const identity = (await userDAO.getUserByEmail(localAuth.email)) as UserShape & UserShapePassword; if (identity && mailer) { // async email @@ -130,8 +130,8 @@ export const resetPassword = async ({ body, t }: any, res: any) => { } const token = Buffer.from(reset.token, 'base64').toString(); - const { email, passwordHash } = jwt.verify(token, secret) as UserShape; - const identity = (await userDAO.getUserByEmail(email)) as UserShape; + const { email, passwordHash } = jwt.verify(token, secret) as UserShape & UserShapePassword; + const identity = (await userDAO.getUserByEmail(email)) as UserShape & UserShapePassword; if (identity.passwordHash !== passwordHash) { throw res.status(401).send(t('user:auth.password.invalidToken')); diff --git a/modules/user/server-ts/social/shared.ts b/modules/user/server-ts/social/shared.ts index ea80be7..8b05857 100644 --- a/modules/user/server-ts/social/shared.ts +++ b/modules/user/server-ts/social/shared.ts @@ -1,5 +1,5 @@ import { access } from '@restapp/authentication-server-ts'; -import UserDAO, { UserShape } from '../sql'; +import UserDAO, { UserShape, UserShapePassword } from '../sql'; export interface UserSocial { id: number; @@ -9,7 +9,7 @@ export interface UserSocial { } export async function onAuthenticationSuccess(req: any, res: any) { - const user = (await UserDAO.getUserWithPassword(req.user.id)) as UserShape; + const user = (await UserDAO.getUserWithPassword(req.user.id)) as UserShape & UserShapePassword; const redirectUrl = req.query.state; const tokens = await access.grantAccess(user, req, user.passwordHash); if (redirectUrl) { diff --git a/modules/user/server-ts/sql.ts b/modules/user/server-ts/sql.ts index 45c0e28..3247659 100644 --- a/modules/user/server-ts/sql.ts +++ b/modules/user/server-ts/sql.ts @@ -5,13 +5,21 @@ import bcrypt from 'bcryptjs'; import { knex, returnId } from '@restapp/database-server-ts'; export interface UserShape { - id: number; + id?: number; username: string; - role: string; - isActive: boolean; - email: string; - passwordHash: string; - userId: number; + role?: string; + isActive?: boolean; + email?: string; + userId?: number; +} + +export interface Profile { + firstName?: string; + lastName?: string; +} + +export interface UserShapePassword { + passwordHash?: string; } interface OrderBy { @@ -31,16 +39,6 @@ interface SocialInterface { userId: number; } -export interface Profile { - firstName?: string; - lastName?: string; - id?: number; - username: string; - email: string; - role?: string; - isActive: boolean; -} - const userColumns = ['u.id', 'u.username', 'u.role', 'u.is_active', 'u.email', 'up.first_name', 'up.last_name']; const userColumnsWithSocial = [ ...userColumns, @@ -130,7 +128,7 @@ class UserDAO { ); } - public register({ username, email, role = 'user', isActive }: Profile, passwordHash?: string | false) { + public register({ username, email, role = 'user', isActive }: UserShape, passwordHash?: string | false) { return knex('user').insert(decamelizeKeys({ username, email, role, passwordHash, isActive })); } @@ -150,7 +148,7 @@ class UserDAO { return returnId(knex('auth_linkedin')).insert({ ln_id: id, display_name: displayName, user_id: userId }); } - public editUser({ id, username, email, role, isActive }: Profile, passwordHash: string) { + public editUser({ id, username, email, role, isActive }: UserShape, passwordHash: string) { const localAuthInput = passwordHash ? { email, passwordHash } : { email }; return knex('user') .update(decamelizeKeys({ username, role, isActive, ...localAuthInput })) From 38be3a84d0ad4a071ca38e8e81206084a02a3a6b Mon Sep 17 00:00:00 2001 From: Ivan Isakov Date: Mon, 20 May 2019 11:50:33 +0300 Subject: [PATCH 073/104] Move session settings to settings module --- config/auth.js | 9 ++++++-- .../server-ts/access/session/index.ts | 22 ++++++++++--------- modules/core/common/clientStorage.ts | 6 ++--- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/config/auth.js b/config/auth.js index a770655..6fe1372 100644 --- a/config/auth.js +++ b/config/auth.js @@ -1,10 +1,15 @@ export default { secret: process.env.NODE_ENV === 'test' ? 'secret for tests' : process.env.AUTH_SECRET, session: { - enabled: true + enabled: true, + secret: 'secret', + store: null, + cookie: { maxAge: 60000 }, + resave: false, + saveUninitialized: false }, jwt: { - enabled: false, + enabled: true, tokenExpiresIn: '1m', refreshTokenExpiresIn: '7d' }, diff --git a/modules/authentication/server-ts/access/session/index.ts b/modules/authentication/server-ts/access/session/index.ts index d2b53aa..c839a27 100644 --- a/modules/authentication/server-ts/access/session/index.ts +++ b/modules/authentication/server-ts/access/session/index.ts @@ -5,21 +5,24 @@ import passport from 'passport'; import { RestMethod } from '@restapp/module-server-ts'; -import { access } from '../../'; import settings from '../../../../../settings'; import AccessModule from '../AccessModule'; import { logout } from './controllers'; const FileStore = require('session-file-store')(session); +const { + auth: { session: sessionSetting } +} = settings; + const beforeware = (app: Express) => { app.use( session({ - secret: 'secret', - store: __DEV__ ? new FileStore() : null, - cookie: { maxAge: 60000 }, - resave: false, - saveUninitialized: false + secret: sessionSetting.secret, + store: __DEV__ ? new FileStore() : sessionSetting.store, + cookie: sessionSetting.cookie, + resave: sessionSetting.resave, + saveUninitialized: sessionSetting.saveUninitialized }) ); app.use(passport.initialize()); @@ -37,7 +40,7 @@ const accessMiddleware = (req: Request, res: Response, next: any) => }); const loginMiddleware = (req: any, res: any, next: any) => { - passport.authenticate('local', { session: settings.auth.session.enabled }, (err, user, info) => { + passport.authenticate('local', { session: sessionSetting.enabled }, (err, user, info) => { if (err || !user) { return res.status(400).json({ errors: { @@ -46,13 +49,12 @@ const loginMiddleware = (req: any, res: any, next: any) => { }); } - req.login(user, { session: settings.auth.session.enabled }, async (loginErr: any) => { + req.login(user, { session: sessionSetting.enabled }, async (loginErr: any) => { if (loginErr) { res.send(loginErr); } - const tokens = settings.auth.jwt.enabled ? await access.grantAccess(user, req, user.passwordHash) : null; - return res.json({ user, tokens }); + return res.json({ user }); }); })(req, res, next); }; diff --git a/modules/core/common/clientStorage.ts b/modules/core/common/clientStorage.ts index e5a6e99..77dc203 100644 --- a/modules/core/common/clientStorage.ts +++ b/modules/core/common/clientStorage.ts @@ -1,3 +1,3 @@ -export const getItem = async (name: string) => window.sessionStorage.getItem(name); -export const setItem = async (name: string, value: string) => window.sessionStorage.setItem(name, value); -export const removeItem = async (name: string) => window.sessionStorage.removeItem(name); +export const getItem = async (name: string) => window.localStorage.getItem(name); +export const setItem = async (name: string, value: string) => window.localStorage.setItem(name, value); +export const removeItem = async (name: string) => window.localStorage.removeItem(name); From f568b2def52fe1ecdb944d4f6645f173fc6c9de9 Mon Sep 17 00:00:00 2001 From: Ivan Isakov Date: Mon, 20 May 2019 12:02:54 +0300 Subject: [PATCH 074/104] Fix check on empty tokens --- config/auth.js | 2 +- modules/user/server-ts/social/shared.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/config/auth.js b/config/auth.js index 6fe1372..ce14dc8 100644 --- a/config/auth.js +++ b/config/auth.js @@ -9,7 +9,7 @@ export default { saveUninitialized: false }, jwt: { - enabled: true, + enabled: false, tokenExpiresIn: '1m', refreshTokenExpiresIn: '7d' }, diff --git a/modules/user/server-ts/social/shared.ts b/modules/user/server-ts/social/shared.ts index 8b05857..1eb0877 100644 --- a/modules/user/server-ts/social/shared.ts +++ b/modules/user/server-ts/social/shared.ts @@ -1,3 +1,4 @@ +import { isEmpty } from 'lodash'; import { access } from '@restapp/authentication-server-ts'; import UserDAO, { UserShape, UserShapePassword } from '../sql'; @@ -13,9 +14,9 @@ export async function onAuthenticationSuccess(req: any, res: any) { const redirectUrl = req.query.state; const tokens = await access.grantAccess(user, req, user.passwordHash); if (redirectUrl) { - res.redirect(redirectUrl + (tokens ? '?data=' + JSON.stringify({ tokens }) : '')); + res.redirect(redirectUrl + (isEmpty(tokens) ? '?data=' + JSON.stringify({ tokens }) : '')); } else { - res.redirect(`/login${tokens ? '?data=' + JSON.stringify({ tokens }) : ''}`); + res.redirect(`/login${isEmpty(tokens) ? '?data=' + JSON.stringify({ tokens }) : ''}`); } } From 84db025dca0244814cefde9cca62f85a32da556e Mon Sep 17 00:00:00 2001 From: Ivan Isakov Date: Mon, 20 May 2019 13:02:41 +0300 Subject: [PATCH 075/104] Add interface for user action creators --- modules/user/client-react/actions/addUser.ts | 13 ++++++----- .../user/client-react/actions/clearUser.ts | 11 +++++---- .../user/client-react/actions/currentUser.ts | 20 ++++++++-------- .../user/client-react/actions/deleteUser.ts | 17 +++++++------- modules/user/client-react/actions/editUser.ts | 13 ++++++----- .../client-react/actions/forgotPassword.ts | 13 ++++++----- modules/user/client-react/actions/index.ts | 22 ++++++++++++++++++ modules/user/client-react/actions/login.ts | 19 +++++++-------- modules/user/client-react/actions/register.ts | 13 ++++++----- .../client-react/actions/resetPassword.ts | 13 ++++++----- modules/user/client-react/actions/user.ts | 17 +++++++------- modules/user/client-react/actions/users.ts | 23 ++++++++++--------- 12 files changed, 113 insertions(+), 81 deletions(-) diff --git a/modules/user/client-react/actions/addUser.ts b/modules/user/client-react/actions/addUser.ts index 53ce677..0913981 100644 --- a/modules/user/client-react/actions/addUser.ts +++ b/modules/user/client-react/actions/addUser.ts @@ -1,9 +1,10 @@ import axios from 'axios'; +import { ActionFunction } from '.'; import { User } from '..'; -export default function ADD_USER(user: User) { - return { - types: {}, - APICall: () => axios.post(`${__API_URL__}/addUser`, { ...user }) - }; -} +const ADD_USER: ActionFunction = user => ({ + types: {}, + APICall: () => axios.post(`${__API_URL__}/addUser`, { ...user }) +}); + +export default ADD_USER; diff --git a/modules/user/client-react/actions/clearUser.ts b/modules/user/client-react/actions/clearUser.ts index 357e0d8..1d9835d 100644 --- a/modules/user/client-react/actions/clearUser.ts +++ b/modules/user/client-react/actions/clearUser.ts @@ -1,7 +1,8 @@ +import { ActionFunction } from '.'; import { ActionType } from '../reducers'; -export default function CLEAR_USER() { - return { - type: ActionType.CLEAR_CURRENT_USER - }; -} +const CLEAR_USER: ActionFunction = () => ({ + type: ActionType.CLEAR_CURRENT_USER +}); + +export default CLEAR_USER; diff --git a/modules/user/client-react/actions/currentUser.ts b/modules/user/client-react/actions/currentUser.ts index 97aac0c..732e856 100644 --- a/modules/user/client-react/actions/currentUser.ts +++ b/modules/user/client-react/actions/currentUser.ts @@ -1,13 +1,13 @@ import axios from 'axios'; import { ActionType } from '../reducers'; +import { ActionFunction } from '.'; -export default function CURRENT_USER() { - return { - types: { - REQUEST: ActionType.CLEAR_CURRENT_USER, - SUCCESS: ActionType.SET_CURRENT_USER, - FAIL: ActionType.CLEAR_CURRENT_USER - }, - APICall: () => axios.get(`${__API_URL__}/currentUser`) - }; -} +const CURRENT_USER: ActionFunction = () => ({ + types: { + REQUEST: ActionType.CLEAR_CURRENT_USER, + SUCCESS: ActionType.SET_CURRENT_USER, + FAIL: ActionType.CLEAR_CURRENT_USER + }, + APICall: () => axios.get(`${__API_URL__}/currentUser`) +}); +export default CURRENT_USER; diff --git a/modules/user/client-react/actions/deleteUser.ts b/modules/user/client-react/actions/deleteUser.ts index ed55d61..28907df 100644 --- a/modules/user/client-react/actions/deleteUser.ts +++ b/modules/user/client-react/actions/deleteUser.ts @@ -1,11 +1,12 @@ import axios from 'axios'; +import { ActionFunction } from '.'; import { ActionType } from '../reducers'; -export default function DELETE_USER(id: number) { - return { - types: { - SUCCESS: ActionType.DELETE_USER - }, - APICall: () => axios.delete(`${__API_URL__}/deleteUser`, { data: { id } }) - }; -} +const DELETE_USER: ActionFunction = id => ({ + types: { + SUCCESS: ActionType.DELETE_USER + }, + APICall: () => axios.delete(`${__API_URL__}/deleteUser`, { data: { id } }) +}); + +export default DELETE_USER; diff --git a/modules/user/client-react/actions/editUser.ts b/modules/user/client-react/actions/editUser.ts index fd4a609..2d7afc4 100644 --- a/modules/user/client-react/actions/editUser.ts +++ b/modules/user/client-react/actions/editUser.ts @@ -1,9 +1,10 @@ import axios from 'axios'; +import { ActionFunction } from '.'; import { User } from '..'; -export default function EDIT_USER(user: User) { - return { - types: {}, - APICall: () => axios.post(`${__API_URL__}/editUser`, { ...user }) - }; -} +const EDIT_USER: ActionFunction = user => ({ + types: {}, + APICall: () => axios.post(`${__API_URL__}/editUser`, { ...user }) +}); + +export default EDIT_USER; diff --git a/modules/user/client-react/actions/forgotPassword.ts b/modules/user/client-react/actions/forgotPassword.ts index 7f5aaf5..0c3ef67 100644 --- a/modules/user/client-react/actions/forgotPassword.ts +++ b/modules/user/client-react/actions/forgotPassword.ts @@ -1,9 +1,10 @@ import axios from 'axios'; +import { ActionFunction } from '.'; import { ForgotPasswordSubmitProps } from '..'; -export default function FORGOT_PASSWORD(value: ForgotPasswordSubmitProps) { - return { - types: {}, - APICall: () => axios.post(`${__API_URL__}/forgotPassword`, { value }) - }; -} +const FORGOT_PASSWORD: ActionFunction = value => ({ + types: {}, + APICall: () => axios.post(`${__API_URL__}/forgotPassword`, { value }) +}); + +export default FORGOT_PASSWORD; diff --git a/modules/user/client-react/actions/index.ts b/modules/user/client-react/actions/index.ts index 7e91e58..1c1e0ed 100644 --- a/modules/user/client-react/actions/index.ts +++ b/modules/user/client-react/actions/index.ts @@ -9,6 +9,28 @@ import EDIT_USER from './editUser'; import ADD_USER from './addUser'; import RESET_PASSWORD from './resetPassword'; import FORGOT_PASSWORD from './forgotPassword'; +import { ActionType } from '../reducers'; + +interface Types { + REQUEST?: ActionType; + SUCCESS?: ActionType; + FAIL?: ActionType; +} +interface ActionCreator { + type?: ActionType; + types?: Types; + APICall?: () => Promise; + payload?: any; +} + +/** + * + * Action description function that will be processed further by redux middleware. + * Must return an object with a @type property if the @APICall property is not in the action. + * If there is an @APICall property then @types property must also be returned. + * + */ +export type ActionFunction

= (param?: P, param2?: P2, param3?: P3) => ActionCreator; export { REGISTER, diff --git a/modules/user/client-react/actions/login.ts b/modules/user/client-react/actions/login.ts index 7e34347..365ef4d 100644 --- a/modules/user/client-react/actions/login.ts +++ b/modules/user/client-react/actions/login.ts @@ -1,13 +1,14 @@ import axios from 'axios'; +import { ActionFunction } from '.'; import { LoginSubmitProps } from '..'; import { ActionType } from '../reducers'; -export default function LOGIN(value: LoginSubmitProps) { - return { - types: { - SUCCESS: ActionType.SET_CURRENT_USER, - FAIL: ActionType.CLEAR_CURRENT_USER - }, - APICall: () => axios.post(`${__API_URL__}/login`, { ...value }) - }; -} +const LOGIN: ActionFunction = value => ({ + types: { + SUCCESS: ActionType.SET_CURRENT_USER, + FAIL: ActionType.CLEAR_CURRENT_USER + }, + APICall: () => axios.post(`${__API_URL__}/login`, { ...value }) +}); + +export default LOGIN; diff --git a/modules/user/client-react/actions/register.ts b/modules/user/client-react/actions/register.ts index b976900..008a656 100644 --- a/modules/user/client-react/actions/register.ts +++ b/modules/user/client-react/actions/register.ts @@ -1,9 +1,10 @@ import axios from 'axios'; +import { ActionFunction } from '.'; import { RegisterSubmitProps } from '..'; -export default function REGISTER(value: RegisterSubmitProps) { - return { - types: {}, - APICall: () => axios.post(`${__API_URL__}/register`, { ...value }) - }; -} +const REGISTER: ActionFunction = value => ({ + types: {}, + APICall: () => axios.post(`${__API_URL__}/register`, { ...value }) +}); + +export default REGISTER; diff --git a/modules/user/client-react/actions/resetPassword.ts b/modules/user/client-react/actions/resetPassword.ts index 5546002..d7570b4 100644 --- a/modules/user/client-react/actions/resetPassword.ts +++ b/modules/user/client-react/actions/resetPassword.ts @@ -1,13 +1,14 @@ import axios from 'axios'; +import { ActionFunction } from '.'; import { ResetPasswordSubmitProps } from '..'; interface ResetPasswordProps extends ResetPasswordSubmitProps { token: string; } -export default function RESET_PASSWORD(value: ResetPasswordProps) { - return { - types: {}, - APICall: () => axios.post(`${__API_URL__}/resetPassword`, { ...value }) - }; -} +const RESET_PASSWORD: ActionFunction = value => ({ + types: {}, + APICall: () => axios.post(`${__API_URL__}/resetPassword`, { ...value }) +}); + +export default RESET_PASSWORD; diff --git a/modules/user/client-react/actions/user.ts b/modules/user/client-react/actions/user.ts index 41d5ca9..24ec0ae 100644 --- a/modules/user/client-react/actions/user.ts +++ b/modules/user/client-react/actions/user.ts @@ -1,11 +1,12 @@ import axios from 'axios'; +import { ActionFunction } from '.'; import { ActionType } from '../reducers'; -export default function USER(id: number) { - return { - types: { - SUCCESS: ActionType.SET_USER - }, - APICall: () => axios.get(`${__API_URL__}/user/${id}`) - }; -} +const USER: ActionFunction = id => ({ + types: { + SUCCESS: ActionType.SET_USER + }, + APICall: () => axios.get(`${__API_URL__}/user/${id}`) +}); + +export default USER; diff --git a/modules/user/client-react/actions/users.ts b/modules/user/client-react/actions/users.ts index 9369ba8..6edf756 100644 --- a/modules/user/client-react/actions/users.ts +++ b/modules/user/client-react/actions/users.ts @@ -1,15 +1,16 @@ import axios from 'axios'; +import { ActionFunction } from '.'; import { ActionType } from '../reducers'; import { OrderBy, Filter } from '..'; -export default function USERS(orderBy: OrderBy, filter: Filter, type?: ActionType) { - return { - types: { - REQUEST: type ? ActionType[type] : null, - SUCCESS: ActionType.SET_USERS, - FAIL: ActionType.CLEAR_USERS - }, - payload: { orderBy, filter }, - APICall: () => axios.post(`${__API_URL__}/users`, { filter, orderBy }) - }; -} +const USERS: ActionFunction = (orderBy: OrderBy, filter: Filter, type: ActionType) => ({ + types: { + REQUEST: type ? ActionType[type] : null, + SUCCESS: ActionType.SET_USERS, + FAIL: ActionType.CLEAR_USERS + }, + payload: { orderBy, filter }, + APICall: () => axios.post(`${__API_URL__}/users`, { filter, orderBy }) +}); + +export default USERS; From fb53859cd2334feb8666798c4c37485b04b3472f Mon Sep 17 00:00:00 2001 From: Ivan Isakov Date: Mon, 20 May 2019 18:58:37 +0300 Subject: [PATCH 076/104] Add and Fix Profile on react-native app --- modules/core/client-react-native/App.tsx | 3 +- modules/core/common/createReduxStore.ts | 3 +- .../{CardLabel.jsx => CardLabel.tsx} | 15 +- .../ui-native-base/components/CardText.jsx | 21 -- .../ui-native-base/components/CardText.tsx | 20 ++ .../ui-native-base/components/ListItem.jsx | 19 -- .../ui-native-base/components/ListItem.tsx | 20 ++ .../ui-native-base/components/SearchBar.jsx | 25 -- .../ui-native-base/components/SearchBar.tsx | 17 ++ .../ui-native-base/components/Select.jsx | 73 ------ .../ui-native-base/components/Select.tsx | 85 ++++++ .../components/{Switch.jsx => Switch.tsx} | 19 +- .../ui-native-base/styles/CardLabel.js | 9 - .../ui-native-base/styles/CardLabel.ts | 15 ++ .../ui-native-base/styles/CardText.js | 7 - .../ui-native-base/styles/CardText.ts | 13 + .../components/ProfileView.native.tsx | 88 +++++++ .../components/UserAddView.native.tsx | 41 +++ .../components/UserEditView.native.tsx | 47 ++++ .../components/UserForm.native.tsx | 209 +++++++++++++++ .../user/client-react/components/UserForm.tsx | 2 - .../components/UsersFilterView.native.tsx | 243 ++++++++++++++++++ .../components/UsersListView.native.tsx | 131 ++++++++++ .../user/client-react/containers/AuthBase.tsx | 28 +- .../containers/Profile.native.tsx | 16 ++ .../user/client-react/containers/Profile.tsx | 10 +- .../user/client-react/containers/UserAdd.tsx | 4 +- .../containers/UserEdit.native.tsx | 61 +++++ .../user/client-react/containers/UserEdit.tsx | 21 +- .../containers/UserScreenNavigator.native.tsx | 5 +- .../client-react/containers/Users.native.tsx | 56 ++++ modules/user/client-react/index.native.tsx | 123 ++++++++- packages/mobile/.zenrc.js | 2 +- 33 files changed, 1234 insertions(+), 217 deletions(-) rename modules/look/client-react-native/ui-native-base/components/{CardLabel.jsx => CardLabel.tsx} (50%) delete mode 100644 modules/look/client-react-native/ui-native-base/components/CardText.jsx create mode 100644 modules/look/client-react-native/ui-native-base/components/CardText.tsx delete mode 100644 modules/look/client-react-native/ui-native-base/components/ListItem.jsx create mode 100644 modules/look/client-react-native/ui-native-base/components/ListItem.tsx delete mode 100644 modules/look/client-react-native/ui-native-base/components/SearchBar.jsx create mode 100644 modules/look/client-react-native/ui-native-base/components/SearchBar.tsx delete mode 100644 modules/look/client-react-native/ui-native-base/components/Select.jsx create mode 100644 modules/look/client-react-native/ui-native-base/components/Select.tsx rename modules/look/client-react-native/ui-native-base/components/{Switch.jsx => Switch.tsx} (57%) delete mode 100644 modules/look/client-react-native/ui-native-base/styles/CardLabel.js create mode 100644 modules/look/client-react-native/ui-native-base/styles/CardLabel.ts delete mode 100644 modules/look/client-react-native/ui-native-base/styles/CardText.js create mode 100644 modules/look/client-react-native/ui-native-base/styles/CardText.ts create mode 100644 modules/user/client-react/components/ProfileView.native.tsx create mode 100644 modules/user/client-react/components/UserAddView.native.tsx create mode 100644 modules/user/client-react/components/UserEditView.native.tsx create mode 100644 modules/user/client-react/components/UserForm.native.tsx create mode 100644 modules/user/client-react/components/UsersFilterView.native.tsx create mode 100644 modules/user/client-react/components/UsersListView.native.tsx create mode 100644 modules/user/client-react/containers/Profile.native.tsx create mode 100644 modules/user/client-react/containers/UserEdit.native.tsx create mode 100644 modules/user/client-react/containers/Users.native.tsx diff --git a/modules/core/client-react-native/App.tsx b/modules/core/client-react-native/App.tsx index 31b35aa..5f4bb45 100644 --- a/modules/core/client-react-native/App.tsx +++ b/modules/core/client-react-native/App.tsx @@ -5,7 +5,6 @@ import url from 'url'; import ClientModule from '@restapp/module-client-react-native'; import log from '../../../packages/common/log'; import createReduxStore from '../../../packages/common/createReduxStore'; - const { protocol, pathname, port } = url.parse(__API_URL__); interface MainProps { @@ -32,7 +31,7 @@ export default class Main extends React.Component { : state => state, {}, // initial state null, - modules.requestMiddlewares + modules.reduxMiddlewares ); log.info(`Connecting to REST backend at: ${apiUrl}`); diff --git a/modules/core/common/createReduxStore.ts b/modules/core/common/createReduxStore.ts index e30cc55..426105b 100644 --- a/modules/core/common/createReduxStore.ts +++ b/modules/core/common/createReduxStore.ts @@ -31,10 +31,11 @@ const requestMiddleware: Middleware = _state => next => action => { ...rest, payload: data }); + return data; } catch (e) { if (e.response && e.response.data && e.response.data.status === 401) { - return next({ ...action, status: e.response.data.status }); + return next({ ...action, type: null, status: e.response.data.status }); } const data = e.response && e.response.data; next({ diff --git a/modules/look/client-react-native/ui-native-base/components/CardLabel.jsx b/modules/look/client-react-native/ui-native-base/components/CardLabel.tsx similarity index 50% rename from modules/look/client-react-native/ui-native-base/components/CardLabel.jsx rename to modules/look/client-react-native/ui-native-base/components/CardLabel.tsx index eea6207..ebd8832 100644 --- a/modules/look/client-react-native/ui-native-base/components/CardLabel.jsx +++ b/modules/look/client-react-native/ui-native-base/components/CardLabel.tsx @@ -1,9 +1,13 @@ import React from 'react'; -import PropTypes from 'prop-types'; -import { Text, StyleSheet } from 'react-native'; +import { Text, StyleSheet, ViewStyle, TextStyle } from 'react-native'; import CardLabelStyles from '../styles/CardLabel'; -const CardLabel = ({ children, style, ...props }) => { +interface CardLabelProps { + children?: string; + style?: ViewStyle | TextStyle; +} + +const CardLabel = ({ children, style, ...props }: CardLabelProps) => { return ( {children.toUpperCase()} @@ -11,11 +15,6 @@ const CardLabel = ({ children, style, ...props }) => { ); }; -CardLabel.propTypes = { - children: PropTypes.string, - style: PropTypes.oneOfType([PropTypes.number, PropTypes.object]) -}; - const styles = StyleSheet.create(CardLabelStyles); export default CardLabel; diff --git a/modules/look/client-react-native/ui-native-base/components/CardText.jsx b/modules/look/client-react-native/ui-native-base/components/CardText.jsx deleted file mode 100644 index bc4d419..0000000 --- a/modules/look/client-react-native/ui-native-base/components/CardText.jsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { Text, StyleSheet } from 'react-native'; -import CardTextStyles from '../styles/CardText'; - -const CardText = ({ children, style, ...props }) => { - return ( - - {children} - - ); -}; - -CardText.propTypes = { - children: PropTypes.string, - style: PropTypes.oneOfType([PropTypes.number, PropTypes.object]) -}; - -const styles = StyleSheet.create(CardTextStyles); - -export default CardText; diff --git a/modules/look/client-react-native/ui-native-base/components/CardText.tsx b/modules/look/client-react-native/ui-native-base/components/CardText.tsx new file mode 100644 index 0000000..9e0f24c --- /dev/null +++ b/modules/look/client-react-native/ui-native-base/components/CardText.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { Text, StyleSheet, TextStyle, ViewStyle } from 'react-native'; +import CardTextStyles from '../styles/CardText'; + +interface CardTextProps { + children?: string; + style?: ViewStyle | TextStyle; +} + +const CardText = ({ children, style, ...props }: CardTextProps) => { + return ( + + {children} + + ); +}; + +const styles = StyleSheet.create(CardTextStyles); + +export default CardText; diff --git a/modules/look/client-react-native/ui-native-base/components/ListItem.jsx b/modules/look/client-react-native/ui-native-base/components/ListItem.jsx deleted file mode 100644 index dbcb759..0000000 --- a/modules/look/client-react-native/ui-native-base/components/ListItem.jsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { ListItem as NBListItem } from 'native-base'; - -const ListItem = ({ children, onPress, onClick, ...props }) => { - return ( - - {children} - - ); -}; - -ListItem.propTypes = { - children: PropTypes.node, - onClick: PropTypes.func, - onPress: PropTypes.func -}; - -export default ListItem; diff --git a/modules/look/client-react-native/ui-native-base/components/ListItem.tsx b/modules/look/client-react-native/ui-native-base/components/ListItem.tsx new file mode 100644 index 0000000..5dbfbc2 --- /dev/null +++ b/modules/look/client-react-native/ui-native-base/components/ListItem.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { ListItem as NBListItem } from 'native-base'; +import { GestureResponderEvent } from 'react-native'; + +interface ListItemProps { + onClick?: (e: GestureResponderEvent) => void; + onPress?: (e: GestureResponderEvent) => void; + children?: React.ReactNode; + style?: any; +} + +const ListItem = ({ children, onPress, onClick, ...props }: ListItemProps) => { + return ( + + {children} + + ); +}; + +export default ListItem; diff --git a/modules/look/client-react-native/ui-native-base/components/SearchBar.jsx b/modules/look/client-react-native/ui-native-base/components/SearchBar.jsx deleted file mode 100644 index 356c152..0000000 --- a/modules/look/client-react-native/ui-native-base/components/SearchBar.jsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { Item, Input, Icon } from 'native-base'; -import { placeholderColor } from '../../styles'; - -const SearchBar = ({ onChange, onChangeText, ...props }) => { - return ( - - - - - ); -}; - -SearchBar.propTypes = { - onChange: PropTypes.func, - onChangeText: PropTypes.func -}; - -export default SearchBar; diff --git a/modules/look/client-react-native/ui-native-base/components/SearchBar.tsx b/modules/look/client-react-native/ui-native-base/components/SearchBar.tsx new file mode 100644 index 0000000..7c15942 --- /dev/null +++ b/modules/look/client-react-native/ui-native-base/components/SearchBar.tsx @@ -0,0 +1,17 @@ +import * as React from 'react'; +import * as ReactNative from 'react-native'; +import { Item, Input, Icon } from 'native-base'; +import { placeholderColor } from '../../styles'; + +interface SearchBarProps extends ReactNative.TextInputProps {} + +const SearchBar = ({ ...props }: SearchBarProps) => { + return ( + + + + + ); +}; + +export default SearchBar; diff --git a/modules/look/client-react-native/ui-native-base/components/Select.jsx b/modules/look/client-react-native/ui-native-base/components/Select.jsx deleted file mode 100644 index a0fae26..0000000 --- a/modules/look/client-react-native/ui-native-base/components/Select.jsx +++ /dev/null @@ -1,73 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { Platform, StyleSheet } from 'react-native'; -import { Picker, Item } from 'native-base'; -import { FontAwesome } from '@expo/vector-icons'; -import SelectStyles from '../styles/Select'; - -const Select = ({ - icon, - iconName, - iconColor, - iconSize, - data, - onValueChange, - selectedValue, - value, - onChange, - style, - itemStyle, - placeholder = '...', - ...props -}) => { - return Platform.OS === 'ios' ? ( - - {icon && ( - - )} - - {data.map((option, idx) => ( - - ))} - - - ) : ( - - - {data.map((option, idx) => ( - - ))} - - - ); -}; - -Select.propTypes = { - data: PropTypes.array.isRequired, - onValueChange: PropTypes.func, - onChange: PropTypes.func, - value: PropTypes.string, - selectedValue: PropTypes.string, - placeholder: PropTypes.string, - icon: PropTypes.bool, - iconName: PropTypes.string, - iconColor: PropTypes.string, - iconSize: PropTypes.number, - style: PropTypes.oneOfType([PropTypes.object, PropTypes.number]), - itemStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.number]) -}; - -const styles = StyleSheet.create(SelectStyles); - -export default Select; diff --git a/modules/look/client-react-native/ui-native-base/components/Select.tsx b/modules/look/client-react-native/ui-native-base/components/Select.tsx new file mode 100644 index 0000000..070e806 --- /dev/null +++ b/modules/look/client-react-native/ui-native-base/components/Select.tsx @@ -0,0 +1,85 @@ +import * as React from 'react'; +import * as ReactNative from 'react-native'; +import { Platform, StyleSheet, ViewStyle, TextStyle } from 'react-native'; +import { Picker, Item } from 'native-base'; +import { FontAwesome } from '@expo/vector-icons'; +import { omit } from 'lodash'; +import SelectStyles from '../styles/Select'; + +interface Option { + value: string; + label: string; +} +interface AntProps { + okText: string; + dismissText: string; + cols: number; + extra: string; +} + +interface SelectProps extends ReactNative.PickerProps, AntProps { + data: Option[]; + onValueChange?: (value: string) => void; + onChange?: (value: string) => void; + value?: string; + selectedValue?: string; + placeholder?: string; + icon?: boolean; + iconName?: string; + iconColor?: string; + iconSize?: number; + style?: ViewStyle | TextStyle; +} + +const Select: React.FunctionComponent = selectProps => { + const { + icon, + iconName, + iconColor, + iconSize, + data, + onValueChange, + selectedValue, + value, + onChange, + style, + itemStyle, + placeholder = '...', + ...props + } = omit(selectProps, ['okText', 'dismissText', 'cols', 'extra']); + return Platform.OS === 'ios' ? ( + + {icon && ( + + )} + + {data.map((option, idx) => ( + + ))} + + + ) : ( + + + {data.map((option, idx) => ( + + ))} + + + ); +}; + +const styles = StyleSheet.create(SelectStyles); + +export default Select; diff --git a/modules/look/client-react-native/ui-native-base/components/Switch.jsx b/modules/look/client-react-native/ui-native-base/components/Switch.tsx similarity index 57% rename from modules/look/client-react-native/ui-native-base/components/Switch.jsx rename to modules/look/client-react-native/ui-native-base/components/Switch.tsx index e136f8c..3e12b08 100644 --- a/modules/look/client-react-native/ui-native-base/components/Switch.jsx +++ b/modules/look/client-react-native/ui-native-base/components/Switch.tsx @@ -1,17 +1,16 @@ import React from 'react'; import { Switch as NBSwitch } from 'native-base'; -import PropTypes from 'prop-types'; -const Switch = ({ color, checked, value, onValueChange, onChange, ...props }) => { - return ; -}; +interface SwitchProps { + checked?: boolean; + value: boolean; + color?: string; + onValueChange?: (value: boolean) => void; + onChange?: (value: boolean) => void; +} -Switch.propTypes = { - checked: PropTypes.bool, - value: PropTypes.bool, - color: PropTypes.string, - onValueChange: PropTypes.func, - onChange: PropTypes.func +const Switch = ({ color, checked, value, onValueChange, onChange, ...props }: SwitchProps) => { + return ; }; export default Switch; diff --git a/modules/look/client-react-native/ui-native-base/styles/CardLabel.js b/modules/look/client-react-native/ui-native-base/styles/CardLabel.js deleted file mode 100644 index 2fca6ea..0000000 --- a/modules/look/client-react-native/ui-native-base/styles/CardLabel.js +++ /dev/null @@ -1,9 +0,0 @@ -const CardLabelStyles = { - text: { - fontSize: 12, - fontWeight: '600', - color: '#686b70' - } -}; - -export default CardLabelStyles; diff --git a/modules/look/client-react-native/ui-native-base/styles/CardLabel.ts b/modules/look/client-react-native/ui-native-base/styles/CardLabel.ts new file mode 100644 index 0000000..947753b --- /dev/null +++ b/modules/look/client-react-native/ui-native-base/styles/CardLabel.ts @@ -0,0 +1,15 @@ +import { ViewStyle, TextStyle } from 'react-native'; + +interface Styles { + [key: string]: ViewStyle | TextStyle; +} + +const CardLabelStyles: Styles = { + text: { + fontSize: 12, + fontWeight: '600', + color: '#686b70' + } +}; + +export default CardLabelStyles; diff --git a/modules/look/client-react-native/ui-native-base/styles/CardText.js b/modules/look/client-react-native/ui-native-base/styles/CardText.js deleted file mode 100644 index b911ca8..0000000 --- a/modules/look/client-react-native/ui-native-base/styles/CardText.js +++ /dev/null @@ -1,7 +0,0 @@ -const CardTextStyles = { - text: { - fontSize: 14 - } -}; - -export default CardTextStyles; diff --git a/modules/look/client-react-native/ui-native-base/styles/CardText.ts b/modules/look/client-react-native/ui-native-base/styles/CardText.ts new file mode 100644 index 0000000..eb2db55 --- /dev/null +++ b/modules/look/client-react-native/ui-native-base/styles/CardText.ts @@ -0,0 +1,13 @@ +import { ViewStyle, TextStyle } from 'react-native'; + +interface Styles { + [key: string]: ViewStyle | TextStyle; +} + +const CardTextStyles: Styles = { + text: { + fontSize: 14 + } +}; + +export default CardTextStyles; diff --git a/modules/user/client-react/components/ProfileView.native.tsx b/modules/user/client-react/components/ProfileView.native.tsx new file mode 100644 index 0000000..502291a --- /dev/null +++ b/modules/user/client-react/components/ProfileView.native.tsx @@ -0,0 +1,88 @@ +import * as React from 'react'; +import { StyleSheet, Text, View, ScrollView, TouchableOpacity } from 'react-native'; +import { translate, TranslateFunction } from '@restapp/i18n-client-react'; +import { Card, CardItem, CardText, CardHeader, CardLabel, Loading } from '@restapp/look-client-react-native'; +import { linkText } from '@restapp/look-client-react-native/styles'; + +import { NavigationOptionsProps, User } from '../index.native'; + +interface ProfileViewProps extends NavigationOptionsProps { + currentUserLoading: boolean; + currentUser: User; + t: TranslateFunction; +} + +type ProfileItemProps = (title: string, value: string, idx: number) => JSX.Element; + +const renderProfileItem: ProfileItemProps = (title, value, idx) => ( + + {`${title}: `} + {value} + +); + +const ProfileView: React.FunctionComponent = ({ currentUserLoading, currentUser, navigation, t }) => { + const profileItems = currentUser + ? [ + { + label: `${t('profile.card.group.name')}`, + value: currentUser.username + }, + { + label: `${t('profile.card.group.email')}`, + value: currentUser.email + }, + { + label: `${t('profile.card.group.role')}`, + value: currentUser.role + } + ] + : []; + + if (currentUser && currentUser.fullName) { + profileItems.push({ label: `${t('profile.card.group.full')}`, value: currentUser.fullName }); + } + + return ( + + {currentUserLoading ? ( + + ) : ( + + + + + {profileItems.map((item, idx) => renderProfileItem(item.label, item.value, idx))} + + + navigation.navigate('ProfileEdit', { id: currentUser.id })} + > + {t('profile.editProfileText')} + + + )} + + ); +}; + +const styles = StyleSheet.create({ + scrollContainer: { + paddingTop: 10, + paddingHorizontal: 20 + }, + container: { + flex: 1 + }, + cardWrapper: { + marginBottom: 15 + }, + linkWrapper: { + alignItems: 'center', + justifyContent: 'center' + }, + linkText +}); + +export default translate('user')(ProfileView); diff --git a/modules/user/client-react/components/UserAddView.native.tsx b/modules/user/client-react/components/UserAddView.native.tsx new file mode 100644 index 0000000..18457e8 --- /dev/null +++ b/modules/user/client-react/components/UserAddView.native.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { StyleSheet, View } from 'react-native'; +import { translate, TranslateFunction } from '@restapp/i18n-client-react'; + +import UserForm from './UserForm'; + +import { User } from '../index.native'; + +interface FormValues extends User { + password: string; + passwordConfirmation: string; +} + +interface UserAddViewProps { + t: TranslateFunction; + onSubmit: (values: FormValues) => Promise; +} + +class UserAddView extends React.PureComponent { + public render() { + return ( + + + + ); + } +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center' + } +}); + +export default translate('user')(UserAddView); diff --git a/modules/user/client-react/components/UserEditView.native.tsx b/modules/user/client-react/components/UserEditView.native.tsx new file mode 100644 index 0000000..f8d81ca --- /dev/null +++ b/modules/user/client-react/components/UserEditView.native.tsx @@ -0,0 +1,47 @@ +import * as React from 'react'; +import { StyleSheet, View } from 'react-native'; +import { translate, TranslateFunction } from '@restapp/i18n-client-react'; +import { Loading } from '@restapp/look-client-react-native'; + +import UserForm from './UserForm'; +import { User, ResetPasswordSubmitProps } from '../index.native'; + +interface FormValues extends User, ResetPasswordSubmitProps {} + +interface UserEditViewProps { + loading: boolean; + user: User; + currentUser: User; + t: TranslateFunction; + onSubmit: (values: FormValues) => Promise; +} + +class UserEditView extends React.PureComponent { + public render() { + const { loading, user, t, currentUser } = this.props; + if (loading && !user) { + return ; + } else { + const isNotSelf = !user || (user && user.id !== currentUser.id); + return ( + + + + ); + } + } +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center' + } +}); + +export default translate('user')(UserEditView); diff --git a/modules/user/client-react/components/UserForm.native.tsx b/modules/user/client-react/components/UserForm.native.tsx new file mode 100644 index 0000000..ab52c05 --- /dev/null +++ b/modules/user/client-react/components/UserForm.native.tsx @@ -0,0 +1,209 @@ +import * as React from 'react'; +import { withFormik, FormikProps } from 'formik'; +import { View, StyleSheet } from 'react-native'; +import KeyboardSpacer from 'react-native-keyboard-spacer'; +import { FieldAdapter as Field } from '@restapp/forms-client-react'; +import { translate, TranslateFunction } from '@restapp/i18n-client-react'; + +import { RenderField, Button, RenderSelect, RenderSwitch, FormView, primary } from '@restapp/look-client-react-native'; +import { placeholderColor, submit } from '@restapp/look-client-react-native/styles'; +import { email as emailRule, minLength, required, match, validate } from '@restapp/validation-common-react'; + +import { FormProps, User, UserRole } from '../index.native'; + +import settings from '../../../../settings'; + +interface FormikFormProps extends FormProps { + initialValues: FormValues; +} + +interface UserFormProps extends FormikProps { + handleSubmit: () => void; + t: TranslateFunction; + handleChange: () => void; + setFieldValue: (type: string, value: any) => void; + onSubmit: (values: FormValues) => Promise; + setTouched: () => void; + isValid: boolean; + error: string; + shouldDisplayRole: boolean; + shouldDisplayActive: boolean; + values: any; + errors: any; + initialValues: FormValues; + touched: any; +} + +interface FormValues extends User { + password: string; + passwordConfirmation: string; +} + +type RoleChangeFunc = ( + type: string, + value: string | string[], + setFieldValue: (type: string, value: string) => void +) => void; + +const userFormSchema = { + username: [required, minLength(3)], + email: [required, emailRule], + password: [required, minLength(settings.auth.password.minLength)], + passwordConfirmation: [match('password'), required, minLength(settings.auth.password.minLength)] +}; + +const handleRoleChange: RoleChangeFunc = (type, value, setFieldValue) => { + const preparedValue = Array.isArray(value) ? value[0] : value; + setFieldValue(type, preparedValue); +}; + +const UserForm: React.FunctionComponent = ({ + values, + handleSubmit, + setFieldValue, + t, + shouldDisplayRole, + shouldDisplayActive +}) => { + const options = [ + { + value: 'user', + label: t('userEdit.form.field.role.user') + }, + { + value: 'admin', + label: t('userEdit.form.field.role.admin') + } + ]; + const { username, email, role, isActive, firstName, lastName, password, passwordConfirmation } = values; + return ( + + + + + {shouldDisplayRole && ( + handleRoleChange('role', value, setFieldValue)} + cols={1} + data={options} + /> + )} + {shouldDisplayActive && ( + setFieldValue('isActive', !isActive)} + component={RenderSwitch} + placeholder={t('userEdit.form.field.active')} + checked={isActive} + placeholderTextColor={placeholderColor} + /> + )} + + + + + + + + + + + + ); +}; + +const UserFormWithFormik = withFormik({ + mapPropsToValues: values => { + const { username, email, role, isActive, firstName, lastName, ...rest } = values.initialValues; + return { + username, + email, + role: role || UserRole.user, + isActive, + password: '', + passwordConfirmation: '', + firstName, + lastName, + ...rest + }; + }, + handleSubmit(values, { setErrors, props: { onSubmit } }) { + onSubmit(values).catch((e: any) => { + if (e && e.errors) { + setErrors(e.errors); + } else { + throw e; + } + }); + }, + displayName: 'SignUpForm ', // helps with React DevTools + validate: values => validate(values, userFormSchema) +}); + +const styles = StyleSheet.create({ + formContainer: { + paddingHorizontal: 20, + justifyContent: 'center', + flex: 1 + }, + submit, + formView: { + flex: 1, + alignSelf: 'stretch' + } +}); + +export default translate('user')(UserFormWithFormik(UserForm)); diff --git a/modules/user/client-react/components/UserForm.tsx b/modules/user/client-react/components/UserForm.tsx index b6cd7fa..79e3dcc 100644 --- a/modules/user/client-react/components/UserForm.tsx +++ b/modules/user/client-react/components/UserForm.tsx @@ -51,8 +51,6 @@ const UserForm: React.FunctionComponent = ({ values, handleSubmit, errors, - setFieldValue, - t, shouldDisplayRole, shouldDisplayActive diff --git a/modules/user/client-react/components/UsersFilterView.native.tsx b/modules/user/client-react/components/UsersFilterView.native.tsx new file mode 100644 index 0000000..20b90d8 --- /dev/null +++ b/modules/user/client-react/components/UsersFilterView.native.tsx @@ -0,0 +1,243 @@ +import * as React from 'react'; +import { View, StyleSheet, Text, TouchableOpacity } from 'react-native'; +import { debounce } from 'lodash'; +import { FontAwesome } from '@expo/vector-icons'; +import { translate, TranslateFunction } from '@restapp/i18n-client-react'; +import { + Select, + SearchBar, + Switch, + List, + ListItem, + success, + danger, + Modal, + Button +} from '@restapp/look-client-react-native'; +import { itemAction, itemContainer, itemTitle } from '@restapp/look-client-react-native/styles'; +import { OrderBy } from '../index.native'; + +interface UsersFilterViewProps { + searchText: string; + role: string; + isActive: boolean; + onSearchTextChange: (value: string) => void; + onRoleChange: (value: string) => void; + onIsActiveChange: (isActive: boolean) => void; + orderBy: OrderBy; + onOrderBy: (order: OrderBy) => void; + t: TranslateFunction; + filter: any; +} + +interface UsersFilterViewState { + showModal: boolean; + orderBy: OrderBy; +} + +type ListItemProps = (label: string, value: string, idx: number) => JSX.Element; + +class UsersFilterView extends React.PureComponent { + private onChangeTextDelayed: (value: string) => void = null; + constructor(props: UsersFilterViewProps) { + super(props); + this.state = { + showModal: false, + orderBy: { + column: '', + order: '' + } + }; + this.onChangeTextDelayed = debounce(this.handleSearch, 500); + } + + public renderOrderByArrow = (name: string) => { + const { orderBy } = this.state; + + if (orderBy && orderBy.column === name) { + if (orderBy.order === 'desc') { + return ; + } else { + return ; + } + } else { + return ; + } + }; + + public orderBy = (name: string) => { + const { orderBy } = this.state; + + let order = 'asc'; + if (orderBy && orderBy.column === name) { + if (orderBy.order === 'asc') { + order = 'desc'; + } else if (orderBy.order === 'desc') { + return this.setState({ + orderBy: { + column: '', + order: '' + } + }); + } + } + return this.setState({ orderBy: { column: name, order } }); + }; + + public renderListItem: ListItemProps = (label, value, idx) => { + return ( + this.orderBy(value)}> + + + {label} + + {this.renderOrderByArrow(value)} + + + ); + }; + + public renderModalChildren = () => { + const { orderBy, t } = this.props; + + const orderByParams = [ + { + label: t('users.column.name'), + value: 'username' + }, + { + label: t('users.column.email'), + value: 'email' + }, + { + label: t('users.column.role'), + value: 'role' + }, + { + label: t('users.column.active'), + value: 'isActive' + } + ]; + return ( + + + {orderByParams.map((item, idx) => this.renderListItem(item.label, item.value, idx))} + + + + + + + + + ); + }; + + public onOrderBy = () => { + this.props.onOrderBy(this.state.orderBy); + this.setState({ showModal: false }); + }; + + public handleSearch = (text: string) => { + const { onSearchTextChange } = this.props; + onSearchTextChange(text); + }; + + public handleRole = (value: string) => { + const { onRoleChange } = this.props; + onRoleChange(value); + }; + + public handleIsActive = () => { + const { + onIsActiveChange, + filter: { isActive } + } = this.props; + onIsActiveChange(!isActive); + }; + + public render() { + const { + filter: { role, isActive }, + t + } = this.props; + const options = [ + { value: '', label: t('users.list.item.role.all') }, + { value: 'user', label: t('users.list.item.role.user') }, + { value: 'admin', label: t('users.list.item.role.admin') } + ]; + return ( + + + + + + {t('users.list.item.role.label')} + +