From 20fdc1168168af20fba6afc875208fadc25098ef Mon Sep 17 00:00:00 2001 From: Danilo Lutz Date: Wed, 16 Feb 2022 09:09:26 -0300 Subject: [PATCH] Feature/password reset and change (#146) * feat: password reset * feat: password reset and password change --- src/app/components/LoggedUser/index.tsx | 30 ++++ src/app/components/LoggedUser/styles.ts | 13 ++ src/app/components/LoggedUserModal/index.tsx | 160 ++++++++++++++++++ src/app/components/LoggedUserModal/styles.ts | 14 ++ src/app/components/Login/index.tsx | 2 +- src/app/components/Person/Create/index.tsx | 2 +- src/app/components/Person/List/index.tsx | 56 +++--- src/app/components/Person/Update/index.tsx | 49 ++++-- src/app/components/StatusBar/index.tsx | 17 +- src/app/components/StatusBar/styles.ts | 22 ++- src/app/styles/global.ts | 19 ++- src/electron/Main.ts | 36 ++++ .../database/repository/UserRepository.ts | 87 +++++++++- src/locales/en-US/common.json | 15 +- src/locales/pt-BR/common.json | 15 +- 15 files changed, 476 insertions(+), 61 deletions(-) create mode 100644 src/app/components/LoggedUser/index.tsx create mode 100644 src/app/components/LoggedUser/styles.ts create mode 100644 src/app/components/LoggedUserModal/index.tsx create mode 100644 src/app/components/LoggedUserModal/styles.ts diff --git a/src/app/components/LoggedUser/index.tsx b/src/app/components/LoggedUser/index.tsx new file mode 100644 index 0000000..8d7ea7f --- /dev/null +++ b/src/app/components/LoggedUser/index.tsx @@ -0,0 +1,30 @@ +import React, { useCallback, useState } from 'react'; +import { FiUser } from 'react-icons/fi'; +import { useAuth } from '../../hooks/auth'; +import LoggedUserModal from '../LoggedUserModal'; + +import { Container } from './styles'; + +const LoggedUser: React.FC = () => { + const { user } = useAuth(); + const [loggedUserModal, setLoggedUserModal] = useState(false); + + const handleloggedUserModal = useCallback(() => { + setLoggedUserModal((oldState) => !oldState); + }, []); + + return ( + <> + + + {user && user.name} + + + + ); +}; + +export default LoggedUser; diff --git a/src/app/components/LoggedUser/styles.ts b/src/app/components/LoggedUser/styles.ts new file mode 100644 index 0000000..7ccc686 --- /dev/null +++ b/src/app/components/LoggedUser/styles.ts @@ -0,0 +1,13 @@ +import { tint } from 'polished'; +import styled from 'styled-components'; + +export const Container = styled.div` + display: flex; + padding: 6px; + height: 100%; + cursor: pointer; + + &:hover { + background-color: ${(props) => tint(0.4 ,props.theme.colors.background)}; + } +`; diff --git a/src/app/components/LoggedUserModal/index.tsx b/src/app/components/LoggedUserModal/index.tsx new file mode 100644 index 0000000..a909693 --- /dev/null +++ b/src/app/components/LoggedUserModal/index.tsx @@ -0,0 +1,160 @@ +import React, { useCallback, useState } from 'react'; +import { FiLogOut, FiRefreshCw } from 'react-icons/fi'; +import ReactModal from 'react-modal'; +import { useToast } from '../../hooks/toast'; +import { useAuth } from '../../hooks/auth'; +import { useNavigate } from 'react-router-dom'; +import i18n from '../../i18n'; +import Button from '../Button'; +import Input from '../Input'; +import { Container, Row } from './styles'; + +interface ModalProps { + isOpen: boolean; + setOpen: () => void; +} + +const LoggedUserModal: React.FC = ({ isOpen, setOpen }) => { + const { user, signOut } = useAuth(); + const { addToast } = useToast(); + const navigate = useNavigate(); + + const [oldPassword, setOldPassword] = useState(''); + const [newPassword, setNewPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + + const handleUpdatePassword = useCallback(() => { + if (!oldPassword) { + addToast({ + title: i18n.t('notifications.warning'), + type: 'error', + description: i18n.t('profile.typePassword'), + }); + return; + } + if (!newPassword) { + addToast({ + title: i18n.t('notifications.warning'), + type: 'error', + description: i18n.t('profile.typeNewPassword'), + }); + return; + } + + if (!confirmPassword) { + addToast({ + title: i18n.t('notifications.warning'), + type: 'error', + description: i18n.t('profile.typeConfirmPassword'), + }); + return; + } + + if (newPassword !== confirmPassword) { + addToast({ + title: i18n.t('notifications.warning'), + type: 'error', + description: i18n.t('profile.newAndConfirmPassword'), + }); + return; + } + + const changed = window.api.sendSync('changePassword', { + entity: 'User', + value: { + userId: user.id, + password: oldPassword, + newPassword, + }, + }) as { id: string }; + + if (!changed) { + addToast({ + title: i18n.t('notifications.warning'), + type: 'error', + description: i18n.t('profile.oldPasswordInvalid'), + }); + return; + } + + addToast({ + title: i18n.t('notifications.success'), + type: 'success', + description: i18n.t('profile.successPasswordChange'), + }); + + setOldPassword(''); + setNewPassword(''); + setConfirmPassword(''); + }, [addToast, confirmPassword, newPassword, oldPassword, user.id]); + + return ( + + +

{user.name}

+ +

{i18n.t('button.changePassword')}

+
+ + setOldPassword(e.target.value)} + /> + + + setNewPassword(e.target.value)} + /> + + + setConfirmPassword(e.target.value)} + /> + + + + + + + + +
+
+ ); +}; + +export default LoggedUserModal; diff --git a/src/app/components/LoggedUserModal/styles.ts b/src/app/components/LoggedUserModal/styles.ts new file mode 100644 index 0000000..1ad21d6 --- /dev/null +++ b/src/app/components/LoggedUserModal/styles.ts @@ -0,0 +1,14 @@ +import styled from 'styled-components'; + +export const Container = styled.div` + display: flex; + flex-direction: column; + + padding: 24px; +`; + +export const Row = styled.div` + display: flex; + flex-direction: column; + padding: 16px; +`; diff --git a/src/app/components/Login/index.tsx b/src/app/components/Login/index.tsx index 3b22774..8e289dc 100644 --- a/src/app/components/Login/index.tsx +++ b/src/app/components/Login/index.tsx @@ -58,7 +58,7 @@ const Login: React.FC = () => {
-
+ e.preventDefault()}> Librarian { return; } - const result = window.api.sendSync('create', { + const result = window.api.sendSync('userCreate', { entity: 'User', value: { name, diff --git a/src/app/components/Person/List/index.tsx b/src/app/components/Person/List/index.tsx index c0bc06d..b195f8e 100644 --- a/src/app/components/Person/List/index.tsx +++ b/src/app/components/Person/List/index.tsx @@ -22,25 +22,24 @@ const PersonList: React.FC = () => { useEffect(() => { try { - setLoading(true); - const response = window.api.sendSync('listPerson', { - entity: 'User', - value: { - where: null, - pageStart: 0, - pageSize: rowsPerPage, - }, - }) as PaginatedSearch; - setList(response.data); - setLoading(false); - } catch (err) { - console.error(err); - } - + setLoading(true); + const response = window.api.sendSync('listPerson', { + entity: 'User', + value: { + where: null, + pageStart: 0, + pageSize: rowsPerPage, + }, + }) as PaginatedSearch; + setList(response.data); + setLoading(false); + } catch (err) { + console.error(err); + } }, [rowsPerPage]); const handleUpdate = (item: Person): void => { - trigger(AppEvent.personTab, { action: Actions.update, value: item}); + trigger(AppEvent.personTab, { action: Actions.update, value: item }); }; const handleRowClick = (item: Person) => { @@ -65,16 +64,25 @@ const PersonList: React.FC = () => { id: 'edit', Cell: (row: Cell) => { return ( - - { event.stopPropagation(); handleUpdate(row.row.original)}} /> - ) - } - } + + {row.row.original.login !== 'admin' && ( + { + event.stopPropagation(); + handleUpdate(row.row.original); + }} + /> + )} + + ); + }, + }, ], - [], + [] ); - const handleSubmit = useCallback( async ({ pageIndex = 0 }: Search) => { try { @@ -102,7 +110,7 @@ const PersonList: React.FC = () => { }); } }, - [addToast, rowsPerPage], + [addToast, rowsPerPage] ); return ( diff --git a/src/app/components/Person/Update/index.tsx b/src/app/components/Person/Update/index.tsx index 602b0ad..172f3fd 100644 --- a/src/app/components/Person/Update/index.tsx +++ b/src/app/components/Person/Update/index.tsx @@ -62,6 +62,7 @@ const PersonUpdate: React.FC<{ item: Person }> = ({ item }) => { const [document, setDocument] = useState(''); const [notes, setNotes] = useState(''); const [contact, setContact] = useState(''); + const [password, setPassword] = useState(''); const [complement, setComplement] = useState(''); const [zipcode, setZipcode] = useState(''); @@ -256,17 +257,22 @@ const PersonUpdate: React.FC<{ item: Person }> = ({ item }) => { return; } - const result = window.api.sendSync('update', { + let insertEntity = { + id: item.id, + name, + login, + language: i18n.language, + notes, + document, + userTypeId: userType.id, + }; + if (password) { + insertEntity = { ...insertEntity, ...{ password } }; + } + + const result = window.api.sendSync('userUpdate', { entity: 'User', - value: { - id: item.id, - name, - login, - language: i18n.language, - notes, - document, - userTypeId: userType.id, - }, + value: insertEntity, }) as { id: string }; ['Contact', 'Address'].map((tableName) => { @@ -317,7 +323,17 @@ const PersonUpdate: React.FC<{ item: Person }> = ({ item }) => { action: Actions.read, value: insertedPerson, }); - }, [addToast, addresses, contacts, document, item.id, login, name, notes]); + }, [ + addToast, + addresses, + contacts, + document, + item.id, + login, + name, + notes, + password, + ]); const handleCityModal = useCallback(() => { setAddingCity((oldState) => !oldState); @@ -372,6 +388,17 @@ const PersonUpdate: React.FC<{ item: Person }> = ({ item }) => { placeholder={i18n.t('person.login')} /> + + setPassword(e.target.value)} + value={password} + required={false} + placeholder={i18n.t('person.password')} + /> + { const [alerts, setAlerts] = useState(); const [alertsPanel, setAlertsPanel] = useState(false); - const { user } = useAuth(); const { getAlerts, addToast } = useToast(); useEffect(() => { @@ -42,13 +40,14 @@ const StatusBar: React.FC = () => { return ( - - {user && user.name} + - - {alerts && alerts.length > 0 ? : } - + + + {alerts && alerts.length > 0 ? : } + + {alertsPanel && } diff --git a/src/app/components/StatusBar/styles.ts b/src/app/components/StatusBar/styles.ts index 0010045..44001c0 100644 --- a/src/app/components/StatusBar/styles.ts +++ b/src/app/components/StatusBar/styles.ts @@ -3,7 +3,7 @@ import styled from 'styled-components'; export const Container = styled.footer` display: flex; - background: ${(props) => tint(0.2 ,props.theme.colors.background)}; + background: ${(props) => tint(0.2, props.theme.colors.background)}; min-height: 2rem; justify-content: space-between; @@ -11,17 +11,23 @@ export const Container = styled.footer` export const StatusItem = styled.div` display: flex; - padding: 6px; align-items: center; height: 100hw; +`; + +export const StatusItemContainer = styled.div` + display: flex; + padding: 6px; + height: 100%; + cursor: pointer; + + &:hover { + background-color: ${(props) => tint(0.4, props.theme.colors.background)}; + } span { - margin-left: 6px; - display: block; + display: flex; + align-items: center; font-size: 12px; - - svg { - cursor: pointer; - } } `; diff --git a/src/app/styles/global.ts b/src/app/styles/global.ts index 7259982..373e3fb 100644 --- a/src/app/styles/global.ts +++ b/src/app/styles/global.ts @@ -126,7 +126,7 @@ export default createGlobalStyle` } } - .city-modal{ + .city-modal, .password-modal { background-color: ${(props) => props.theme.colors.card.background}; position: absolute; top: 10%; @@ -141,7 +141,22 @@ export default createGlobalStyle` z-index: 1000; } - .modal-overlay{ + .password-modal { + overflow-y: scroll; + } + + .password-modal::-webkit-scrollbar { + border-radius: 8px; + width: 8px; + background: ${(props) => props.theme.colors.input.background}; + } + + .password-modal::-webkit-scrollbar-thumb { + border-radius: 6px; + background: ${(props) => tint(0.2, props.theme.colors.card.background)}; + } + + .modal-overlay { background-color: ${(props) => rgba(props.theme.colors.text, 0.4)}; position: fixed; top: 0; diff --git a/src/electron/Main.ts b/src/electron/Main.ts index 497a358..ae00640 100644 --- a/src/electron/Main.ts +++ b/src/electron/Main.ts @@ -302,6 +302,42 @@ export default class Main { } }); + ipcMain.on('userCreate', async (event, content: Event[]) => { + try { + const { value, entity } = content[0]; + event.returnValue = await this.getCustomRepository( + entity, + UserRepository + ).create(value); + } catch (err) { + log.error(err); + } + }); + + ipcMain.on('userUpdate', async (event, content: Event[]) => { + try { + const { value, entity } = content[0]; + event.returnValue = await this.getCustomRepository( + entity, + UserRepository + ).update(value); + } catch (err) { + log.error(err); + } + }); + + ipcMain.on('changePassword', async (event, content: Event[]) => { + try { + const { value, entity } = content[0]; + event.returnValue = await this.getCustomRepository( + entity, + UserRepository + ).changePassword(value); + } catch (err) { + log.error(err); + } + }); + ipcMain.on('listEdition', async (event, content: Event[]) => { try { const { value, entity } = content[0]; diff --git a/src/electron/database/repository/UserRepository.ts b/src/electron/database/repository/UserRepository.ts index d14b13c..60493e7 100644 --- a/src/electron/database/repository/UserRepository.ts +++ b/src/electron/database/repository/UserRepository.ts @@ -7,13 +7,98 @@ interface Where { password: string; } +interface Create { + name: string; + login: string; + password?: string; + language: string; + notes: string; + document: string; + userTypeId: string; +} + +interface Update extends Create { + id: string; +} +interface ChangePassword { + userId: string; + password: string; + newPassword: string; +} + export default class UserRepository extends RepositoryBase { + public async create(content: Create): Promise { + try { + const entity: Create = { + name: content.name, + login: content.login, + password: await bcrypt.hash(content.password, 12), + language: content.language, + notes: content.notes, + document: content.document, + userTypeId: content.userTypeId, + }; + + const item = await this.repository.create(entity); + return await this.repository.save(item); + } catch (err) { + console.log(err); + throw err; + } + } + + public async update(content: Update): Promise { + try { + let entity: Update = { + id: content.id, + name: content.name, + login: content.login, + language: content.language, + notes: content.notes, + document: content.document, + userTypeId: content.userTypeId, + }; + + if (content.password) { + entity = { + ...entity, + ...{ password: await bcrypt.hash(content.password, 12) }, + }; + } + + return await this.repository.save(entity); + } catch (err) { + console.log(err); + throw err; + } + } + public async login(content: Where): Promise { try { + const user = (await this.repository.findOne(content.where)) as User; + + if (user && bcrypt.compareSync(content.password, user.password)) { + return user; + } - const user = await this.repository.findOne(content.where) as User; + return false; + } catch (err) { + console.log(err); + throw err; + } + } + + public async changePassword( + content: ChangePassword + ): Promise { + try { + const user = (await this.repository.findOne({ + where: { id: content.userId }, + })) as User; if (user && bcrypt.compareSync(content.password, user.password)) { + user.password = await bcrypt.hash(content.newPassword, 12); + await this.repository.save(user); return user; } diff --git a/src/locales/en-US/common.json b/src/locales/en-US/common.json index a54431b..8918873 100644 --- a/src/locales/en-US/common.json +++ b/src/locales/en-US/common.json @@ -48,7 +48,9 @@ "save": "Save", "remove": "Remove", "add": "Add", - "create": "Create" + "create": "Create", + "logout": "Logout", + "changePassword": "Change password" }, "home": { "message": "Hello World!" @@ -184,7 +186,14 @@ "profile": { "label": "Profile", "selectOrCreate": "Select a profile or type a name to create it", - "selectEmpty": "No profile created so far" + "selectEmpty": "No profile created so far", + "informError": "Please provide information to change password: #errors#", + "typePassword": "Type password", + "typeNewPassword": "Type new password", + "typeConfirmPassword": "Type password confirmation", + "oldPasswordInvalid": "Invalid current password", + "newAndConfirmPassword": "New password and it's confirmation don't match", + "successPasswordChange": "Sucessfully password change" }, "userType": { "label": "User Type", @@ -199,6 +208,8 @@ "name": "Name", "login": "Login", "password": "Password", + "newPassword": "New password", + "confirmPassword": "Confirm password", "language": "Language", "notes": "Notes", "document": "Document", diff --git a/src/locales/pt-BR/common.json b/src/locales/pt-BR/common.json index 43495a3..bdae7fd 100644 --- a/src/locales/pt-BR/common.json +++ b/src/locales/pt-BR/common.json @@ -48,7 +48,9 @@ "save": "Salvar", "remove": "Remover", "add": "Adicionar", - "create": "Criar" + "create": "Criar", + "logout": "Sair", + "changePassword": "Trocar senha" }, "home": { "message": "Olá Mundo!" @@ -186,7 +188,14 @@ "profile": { "label": "Perfil", "selectOrCreate": "Selecione um perfil ou escreva um nome para cria-lo", - "selectEmpty": "Nenhum perfil criado" + "selectEmpty": "Nenhum perfil criado", + "informError": "Por favor forneça as informações para alterar a senha: #errors#", + "typePassword": "Digite a senha", + "typeNewPassword": "Digite a nova senha", + "typeConfirmPassword": "Digite a confirmação de senha", + "oldPasswordInvalid": "Senha atual inválida", + "newAndConfirmPassword": "Nova senha e sua confirmação não conferem", + "successPasswordChange": "Senha alterada com sucesso" }, "userType": { "label": "Tipo de Usuário", @@ -201,6 +210,8 @@ "name": "Nome", "login": "Login", "password": "Senha", + "newPassword": "Nova senha", + "confirmPassword": "Confirmar senha", "language": "Idioma", "notes": "Observações", "document": "Documento",