diff options
author | 2022-05-29 17:45:54 +0100 | |
---|---|---|
committer | 2022-05-29 17:45:54 +0100 | |
commit | 308d07785f811ff470d0e90b11680926a823027b (patch) | |
tree | 7afe7859fc29f6a5c34be8b7b486cc317cc82e21 | |
parent | 50d781d0bdcac217f2bc037abe087a00019edba5 (diff) | |
download | my_apps-308d07785f811ff470d0e90b11680926a823027b.tar.gz my_apps-308d07785f811ff470d0e90b11680926a823027b.tar.bz2 my_apps-308d07785f811ff470d0e90b11680926a823027b.zip |
add change password option
-rw-r--r-- | apps/ChangePassword/api.js | 9 | ||||
-rw-r--r-- | apps/ChangePassword/components/App.js | 113 | ||||
-rw-r--r-- | apps/ChangePassword/index.js | 3 | ||||
-rw-r--r-- | apps/ChangePassword/styles/ChangePassword.module.scss | 27 | ||||
-rw-r--r-- | apps/Settings/api.js | 4 | ||||
-rw-r--r-- | apps/index.js | 1 | ||||
-rw-r--r-- | components/Header.js | 28 | ||||
-rw-r--r-- | configs/appList.js | 3 | ||||
-rw-r--r-- | configs/sendMail.js | 2 | ||||
-rw-r--r-- | configs/translations.js | 44 | ||||
-rw-r--r-- | helpers/email.js | 20 | ||||
-rw-r--r-- | models/User.js | 23 | ||||
-rw-r--r-- | pages/api/password.js | 35 | ||||
-rw-r--r-- | pages/api/register.js | 4 | ||||
-rw-r--r-- | pages/api/settings.js | 1 | ||||
-rw-r--r-- | pages/api/verify.js | 4 |
16 files changed, 300 insertions, 21 deletions
diff --git a/apps/ChangePassword/api.js b/apps/ChangePassword/api.js new file mode 100644 index 0000000..ef609a7 --- /dev/null +++ b/apps/ChangePassword/api.js @@ -0,0 +1,9 @@ +import fetchJson from 'helpers/fetchJson' + +export const changePassword = async (data) => ( + await fetchJson('/api/password', { + method: 'POST', + headers: { 'Content-Type': 'plain/text; charset=utf-8' }, + body: JSON.stringify(data) + }) +) diff --git a/apps/ChangePassword/components/App.js b/apps/ChangePassword/components/App.js new file mode 100644 index 0000000..ce32ffb --- /dev/null +++ b/apps/ChangePassword/components/App.js @@ -0,0 +1,113 @@ +import styles from '../styles/ChangePassword.module.scss' +import { useState } from 'react' +import useSettings from 'hooks/useSettings' +import usePopup from 'hooks/usePopup' +import useApps from 'hooks/useApps' +import useUser from 'hooks/useUser' +import { close } from 'helpers/windowActions' +import { changePassword } from '../api' + +const App = () => { + const { t } = useSettings() + const { user } = useUser() + const { setApps } = useApps() + const { setPopup } = usePopup() + const [currPassError, setCurrPassError] = useState(false) + const [newPassError, setNewPassError] = useState(false) + + const onSubmit = async (e) => { + e.preventDefault() + + const { + current_password: {value: currentPassword}, + new_password: { value: newPassword}, + confirm_password: {value: confirmPassword} + } = e.currentTarget + + if (newPassword !== confirmPassword) { + setPopup({ + content: t('change_password_wrong_new'), + time: 2000, + error: true + }) + setNewPassError(true) + + } else if (newPassword === currentPassword) { + setPopup({ + content: t('change_password_same_password'), + time: 2000, + error: true + }) + setNewPassError(true) + } else { + try { + await changePassword({_id: user._id, currentPassword, newPassword}) + setPopup({ + content: t('change_password_changed'), + time: 2000, + }) + close('ChangePassword', setApps) + } catch(e) { + if (e?.data?.error === 'Wrong password') { + setPopup({ + content: t('change_password_wrong_current'), + time: 2000, + error: true + }) + setCurrPassError(true) + } else { + setPopup({ + content: t('change_password_error'), + time: 2000, + error: true + }) + } + } + } + } + + return ( + <form className={styles.password} onSubmit={onSubmit}> + <label className={styles.label} htmlFor='current_password'> + {t('change_password_current')} + </label> + <input + className={`${styles.input} ${currPassError ? styles.error : ''}`} + id='current_password' + type='password' + name='current_password' + title={t('change_password_current')} + onChange={() => { currPassError && setCurrPassError(false) }} + required + /> + <label className={styles.label} htmlFor='new_password'> + {t('change_password_new')} + </label> + <input + className={`${styles.input} ${newPassError ? styles.error : ''}`} + id='new_password' + type='password' + name='new_password' + title={t('change_password_new')} + onChange={() => { newPassError && setNewPassError(false) }} + required + /> + <input + className={`${styles.input} ${newPassError ? styles.error : ''}`} + type='password' + name='confirm_password' + placeholder={t('change_password_confirm')} + title={t('change_password_confirm')} + onChange={() => { newPassError && setNewPassError(false) }} + required + /> + <input + className='window__button' + type='submit' + value={t('save')} + /> + </form> + ) +} + +export default App diff --git a/apps/ChangePassword/index.js b/apps/ChangePassword/index.js new file mode 100644 index 0000000..cd1d5e2 --- /dev/null +++ b/apps/ChangePassword/index.js @@ -0,0 +1,3 @@ +import ChangePassword from './components/App' + +export default ChangePassword diff --git a/apps/ChangePassword/styles/ChangePassword.module.scss b/apps/ChangePassword/styles/ChangePassword.module.scss new file mode 100644 index 0000000..cb4e876 --- /dev/null +++ b/apps/ChangePassword/styles/ChangePassword.module.scss @@ -0,0 +1,27 @@ +.password { + padding: 1em; + text-align: center; +} + +.input { + color: var(--color-text-alt); + background: var(--color-window-content); + padding: .5em; + margin: .75em 0; + border: 1px dashed var(--color-decor); + border-radius: .5px; + width: 100%; +} + +.label { + font-size: .9em; + display: block; + text-align: left; + color: var(--color-text-alt); + margin-top: .75em; +} + +.error { + border: 1px solid var(--color-error); + color: var(--color-error); +} diff --git a/apps/Settings/api.js b/apps/Settings/api.js index f538368..142afc1 100644 --- a/apps/Settings/api.js +++ b/apps/Settings/api.js @@ -1,9 +1,9 @@ import fetchJson from 'helpers/fetchJson' -export const saveSettings = async (data) => { +export const saveSettings = async (data) => ( fetchJson('/api/settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }) -} +) diff --git a/apps/index.js b/apps/index.js index eccedac..05ad5e4 100644 --- a/apps/index.js +++ b/apps/index.js @@ -2,4 +2,5 @@ export { default as Calculator } from './Calculator' export { default as Notes } from './Notes' export { default as Player } from './Player' export { default as Settings } from './Settings' +export { default as ChangePassword } from './ChangePassword' export { default as Youtube } from './Youtube' diff --git a/components/Header.js b/components/Header.js index 2db8178..60bf1bd 100644 --- a/components/Header.js +++ b/components/Header.js @@ -98,15 +98,25 @@ const Header = () => { <div className={styles.headerOverlay} onClick={() => setUserMenu(false)} /> <ul className={styles.submenu}> {user.isVerified && ( - <li> - <span onClick={() => { - open({ appName: 'Settings', ...appList.Settings }, setApps) - setUserMenu() - }} - > - {t('Settings')} - </span> - </li> + <> + <li> + <span onClick={() => { + open({ appName: 'ChangePassword', ...appList.ChangePassword }, setApps) + setUserMenu() + }}> + {t('ChangePassword')} + </span> + </li> + <li> + <span onClick={() => { + open({ appName: 'Settings', ...appList.Settings }, setApps) + setUserMenu() + }} + > + {t('Settings')} + </span> + </li> + </> )} <li> <span onClick={e => handleLogout(e, router, mutateUser)}> diff --git a/configs/appList.js b/configs/appList.js index 13ca6d6..7963648 100644 --- a/configs/appList.js +++ b/configs/appList.js @@ -1,9 +1,10 @@ -import { Calculator, Notes, Player, Settings, Youtube } from 'apps' +import { Calculator, Notes, Player, Settings, ChangePassword, Youtube } from 'apps' const appList = { Calculator: { component: Calculator, icon: true, buttons: ['min', 'close'], height: '30em', width: '20em' }, Notes: { component: Notes, icon: true, buttons: ['min', 'max', 'close'], height: '48em', width: '64em' }, Player: { component: Player, icon: true, buttons: ['min', 'max', 'close'], height: '48em', width: '64em' }, + ChangePassword: { component: ChangePassword, icon: false, buttons: ['min', 'close'], height: '21em', width: '18em' }, Settings: { component: Settings, icon: false, buttons: ['min', 'close'], height: '23em', width: '16em' }, Youtube: { component: Youtube, icon: true, buttons: ['min', 'max', 'close'], height: '48em', width: '64em' } } diff --git a/configs/sendMail.js b/configs/sendMail.js index 6405607..c026f0c 100644 --- a/configs/sendMail.js +++ b/configs/sendMail.js @@ -1,6 +1,6 @@ import nodemailer from 'nodemailer' -const sendMail = (to, subject, text, html) => { +const sendMail = ({to, subject, text, html}) => { const transporter = nodemailer.createTransport({ port: 465, host: process.env.MYAPPS_MAIL_SMTP_SERVER, diff --git a/configs/translations.js b/configs/translations.js index 1b8ed40..b496719 100644 --- a/configs/translations.js +++ b/configs/translations.js @@ -36,12 +36,15 @@ const translations = { Calculator: 'Calculator', Notes: 'Notes', Player: 'Player', + ChangePassword: 'Change Password', Settings: 'Settings', Youtube: 'Youtube', mail_ver_subject: 'Verification of your new apps.pruss.it account', mail_ver_t1: 'Thank you for creating an account in My Apps.', mail_ver_t2: 'We are sending you the verification code:', mail_ver_t3: 'To finish verification log in and paste this code.', + mail_pass_subject: 'Your password has been changed', + mail_pass_t1: 'The password to your apps.pruss.it account was successfully changed.', verification_mail_error: 'Could not send verification email', verification_error: 'Could not verify user', verification_title: 'One last step missing!', @@ -79,6 +82,14 @@ const translations = { player_playlist_default: 'Default playlist', player_item_playing: 'Playing in Player', player_item_enqueued: 'Enqueued in Player', + change_password_current: 'Current password', + change_password_new: 'New password', + change_password_confirm: 'Confirm new password', + change_password_changed: 'Password successfully changed', + change_password_wrong_current: 'Wrong current password', + change_password_wrong_new: 'New passwords do not match', + change_password_same_password: 'New password cannot be the same like previous password', + change_password_error: 'Error while changing password', yt_video: 'Videos', yt_video_count: ' videos', yt_live: 'Live', @@ -125,11 +136,14 @@ const translations = { Notes: 'Notatki', Player: 'Odtwarzacz', Settings: 'Ustawienia', + ChangePassword: 'Zmień hasło', Youtube: 'Youtube', mail_ver_subject: 'Weryfikacja Twojego nowego konta w apps.pruss.it.', mail_ver_t1: 'Dziękujemy za założenie konta w My Apps', mail_ver_t2: 'Przesyłamy Ci kod weryfikacyjny:', mail_ver_t3: 'W celu zakończenia weryfikacji zaloguj się i podaj ten kod.', + mail_pass_subject: 'Twoje hasło zostało zmienione', + mail_pass_t1: 'Hasło do Twojego konta apps.pruss.it zostało zmienione.', verification_mail_error: 'Błąd podczas wysyłania maila z kodem weryfikacyjnym', verification_error: 'Błąd podczas weryfikacji użytkownika', verification_title: 'Pozostał ostatni krok!', @@ -167,6 +181,14 @@ const translations = { player_playlist_default: 'Domyślna lista', player_item_playing: 'Odtwarzanie w Odtwarzaczu', player_item_enqueued: 'Dodano do kolejki odtwarzania', + change_password_current: 'Obecne hasło', + change_password_new: 'Nowe hasło', + change_password_confirm: 'Powtórz nowe hasło', + change_password_changed: 'Hasło zostało zmienione', + change_password_wrong_current: 'Błędne obecne hasło', + change_password_wrong_new: 'Nowe hasła nie zgadzają się', + change_password_same_password: 'Nowe hasło musi różnić się od poprzedniego', + change_password_error: 'Bład podczas zmiany hasła', yt_video: 'Wideo', yt_video_count: ' klipów', yt_live: 'Na żywo', @@ -213,11 +235,14 @@ const translations = { Notes: 'Notas', Player: 'Jugador', Settings: 'Ajustes', + ChangePassword: 'Cambio de contraseña', Youtube: 'Youtube', mail_ver_subject: 'Verificación de su nueva cuenta en apps.pruss.it.', mail_ver_t1: 'Gracias por crear una cuenta con Mis aplicaciones', mail_ver_t2: 'Te enviamos un código de verificación: ', mail_ver_t3: 'Para completar la verificación, inicie sesión e ingrese este código.', + mail_pass_subject: 'Tu contraseña ha sido cambiada', + mail_pass_t1: 'La contraseña de su cuenta apps.pruss.it se cambió con éxito.', verification_mail_error: 'Error al enviar el correo electrónico con el código de verificación', verification_error: 'Error de verificación del usuario', verification_title: '¡Queda un último paso!', @@ -255,6 +280,14 @@ const translations = { player_playlist_default: 'Lista predeterminada', player_item_playing: 'Jugar con el Jugador', player_item_enqueued: 'Añadido a la cola de reproducción', + change_password_current: 'Contraseña actual', + change_password_new: 'Contraseña nueva', + change_password_confirm: 'Repetir contraseña nueva', + change_password_changed: 'La contraseña ha sido cambiada', + change_password_wrong_current: 'Contraseña actual incorrecta', + change_password_wrong_new: 'Las nuevas contraseñas no coinciden', + change_password_same_password: 'La nueva contraseña debe ser diferente a la anterior', + change_password_error: 'Error al cambiar la contraseña', yt_video: 'Video', yt_video_count: ' clips', yt_live: 'En vivo', @@ -301,11 +334,14 @@ const translations = { Notes: 'Anmerkungen', Player: 'Spieler', Settings: 'Einstellungen', + ChangePassword: 'Passwortänderung', Youtube: 'Youtube', mail_ver_subject: 'Bestätigen Ihres neuen Kontos in der apps.pruss.it.', mail_ver_t1: 'Vielen Dank, dass Sie ein Konto bei My Apps erstellt haben', mail_ver_t2: 'Wir senden Ihnen einen Bestätigungscode: ', mail_ver_t3: 'Um die Verifizierung abzuschließen, loggen Sie sich ein und geben Sie diesen Code ein.', + mail_pass_subject: 'Ihr Passwort wurde geändert', + mail_pass_t1: 'Das Passwort für Ihr apps.pruss.it-Konto wurde erfolgreich geändert.', verification_mail_error: 'Fehler beim Senden der E-Mail mit dem Bestätigungscode', verification_error: 'Fehler bei der Benutzerüberprüfung', verification_title: 'Ein letzter Schritt bleibt!', @@ -343,6 +379,14 @@ const translations = { player_playlist_default: 'Standardliste', player_item_playing: 'Spiele auf dem Spieler', player_item_enqueued: 'Zur Play Queue hinzugefügt', + change_password_current: 'Aktuelles Passwort', + change_password_new: 'Neues Passwort', + change_password_confirm: 'Wiederhole neues Passwort', + change_password_changed: 'Passwort erfolgreich geändert', + change_password_wrong_current: 'Falsches aktuelles Passwort', + change_password_wrong_new: 'Neue Passwörter stimmen nicht überein, Neue Passwörter passen nicht', + change_password_same_password: 'Das neue Passwort darf nicht mit dem vorherigen Passwort identisch sein', + change_password_error: 'Fehler beim Ändern des Passworts, Fehler beim Passwort ändern', yt_video: 'Video', yt_video_count: ' Clips', yt_live: 'Live', diff --git a/helpers/email.js b/helpers/email.js index 52815f5..5373d8a 100644 --- a/helpers/email.js +++ b/helpers/email.js @@ -1,5 +1,19 @@ import { t } from 'configs/translations' -export const subject = l => t(l, 'mail_ver_subject') -export const text = (l, key) => `${t(l, 'mail_ver_t1')}\n${t(l, 'mail_ver_t2')}\n\n${key}\n\n${t(l, 'mail_ver_t3')}` -export const html = (l, key) => `<p>${t(l, 'mail_ver_t1')}<br/>${t(l, 'mail_ver_t2')}</p><p style="font-size: 150%;padding: 1em;border: 1px solid black">${key}</p><p>${t(l, 'mail_ver_t3')}</p></br>` +const mailData = (type, l, d) => { + switch(type) { + case 'v': return { + subject: t(l, 'mail_ver_subject'), + text: `${t(l, 'mail_ver_t1')}\n${t(l, 'mail_ver_t2')}\n\n${d}\n\n${t(l, 'mail_ver_t3')}`, + html: `<p>${t(l, 'mail_ver_t1')}<br/>${t(l, 'mail_ver_t2')}</p><p style="font-size: 150%;padding: 1em;border: 1px solid black">${key}</p><p>${t(l, 'mail_ver_t3')}</p></br>`, + } + case 'p': return { + subject: t(l, 'mail_pass_subject'), + text: `${t(l, 'mail_pass_t1')}\n`, + html: `<p>${t(l, 'mail_pass_t1')}<br/></p></br>`, + } + default: + } +} + +export default mailData diff --git a/models/User.js b/models/User.js index bcf2523..4992b20 100644 --- a/models/User.js +++ b/models/User.js @@ -97,6 +97,29 @@ userSchema.statics.saveSettings = async function ({ _id, theme, language }) { return userResponse(user) } +userSchema.statics.savePassword = async function ({ _id, currentPassword, newPassword }) { + const user = await User.findOne({ _id }) + + if (!user) { + throw new Error('Unable to change password') + } + + const isMatch = await bcrypt.compare(currentPassword, user.password) + + if (!isMatch) { + throw new Error('Wrong password') + } + + const password = await bcrypt.hash(newPassword, 8) + const newUser = await User.findOneAndUpdate({ _id }, { password }, { new: true }) + + if (!newUser) { + throw new Error('Could not update password') + } + + return userResponse(newUser) +} + userSchema.statics.state = async function (_id) { const user = await User.findOne({ _id }) diff --git a/pages/api/password.js b/pages/api/password.js new file mode 100644 index 0000000..871c9e6 --- /dev/null +++ b/pages/api/password.js @@ -0,0 +1,35 @@ +import dbConnect from 'configs/dbConnect' +import withSession from 'hocs/withSession' +import sendMail from 'configs/sendMail' +import mailData from 'helpers/email' +import User from 'models/User' + +export default withSession(async (req, res) => { + await dbConnect() + + switch (req.method) { + case 'POST': + try { + const data = JSON.parse(req.body) + if (data._id && data.currentPassword && data.newPassword) { + const user = await User.savePassword(data) + req.session.set('user', user) + await req.session.save() + + sendMail({to: user.email, ...mailData('p', user.language)}) + + res.status(200).json(user) + } + } catch (error) { + if (error?.message === 'Wrong password') { + res.status(401).send({ error: 'Wrong password' }) + } else { + res.status(400).send({ error }) + } + } + break + default: + res.status(400).send({ error }) + break + } +}) diff --git a/pages/api/register.js b/pages/api/register.js index 2c866de..56c8ba9 100644 --- a/pages/api/register.js +++ b/pages/api/register.js @@ -1,7 +1,7 @@ import dbConnect from 'configs/dbConnect' import sendMail from 'configs/sendMail' import withSession from 'hocs/withSession' -import { subject, text, html } from 'helpers/email' +import mailData from 'helpers/email' import User from 'models/User' import NoteList from 'apps/Notes/models/NoteList' @@ -20,7 +20,7 @@ export default withSession(async (req, res) => { if (!email) { throw new Error('Something went wrong') } - sendMail(body.email, subject(language), text(language, key), html(language, key)) + sendMail({to: body.email, ...mailData('v', language, key)}) const user = { _id, email, noteList, theme, language, isVerified: false, isLoggedIn: true } req.session.set('user', user) diff --git a/pages/api/settings.js b/pages/api/settings.js index f0d8e06..868f957 100644 --- a/pages/api/settings.js +++ b/pages/api/settings.js @@ -15,7 +15,6 @@ export default withSession(async (req, res) => { res.status(200).json(user) } } catch (error) { - console.log(error) res.status(400).send() } break diff --git a/pages/api/verify.js b/pages/api/verify.js index 89bd4cd..edcc7a9 100644 --- a/pages/api/verify.js +++ b/pages/api/verify.js @@ -2,7 +2,7 @@ import dbConnect from 'configs/dbConnect' import sendMail from 'configs/sendMail' import User from 'models/User' import withSession from 'hocs/withSession' -import { subject, text, html } from 'helpers/email' +import mailData from 'helpers/email' export default withSession(async (req, res) => { await dbConnect() @@ -16,7 +16,7 @@ export default withSession(async (req, res) => { const key = await User.getVerificationKey(email) if (!key) { throw new Error('Something went wrong') } - const response = await sendMail(email, subject(l), text(l, key), html(l, key)) + const response = await sendMail({to: email, ...mailData('v', l, key)}) if (!response?.accepted?.length) { throw new Error('Something went wrong') } res.status(204).send() |