diff --git a/jam-ui/src/components/common/JKModalDialog.js b/jam-ui/src/components/common/JKModalDialog.js index 16c0377ca..15821ba45 100644 --- a/jam-ui/src/components/common/JKModalDialog.js +++ b/jam-ui/src/components/common/JKModalDialog.js @@ -3,7 +3,8 @@ import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap'; import { useTranslation } from 'react-i18next'; import PropTypes from 'prop-types'; -const JKModalDialog = ({ title, children, show, onToggle, showFooter }) => { +const JKModalDialog = (args) => { + const { show, title, children, onToggle, showFooter, ...rest } = args; const [modal, setModal] = useState(show); const toggle = () => { @@ -18,7 +19,7 @@ const JKModalDialog = ({ title, children, show, onToggle, showFooter }) => { }, [show]); return ( - + {title} {children} {showFooter && ( diff --git a/jam-ui/src/components/dashboard/JKDashboardMain.js b/jam-ui/src/components/dashboard/JKDashboardMain.js index 977f4beae..3afb8e6ed 100644 --- a/jam-ui/src/components/dashboard/JKDashboardMain.js +++ b/jam-ui/src/components/dashboard/JKDashboardMain.js @@ -35,6 +35,7 @@ import JKNewMusicSession from '../page/JKNewMusicSession'; import JKMusicSessionsLobby from '../page/JKMusicSessionsLobby'; import JKEditProfile from '../page/JKEditProfile'; +import JKEditAccount from '../page/JKEditAccount'; //import loadable from '@loadable/component'; //const DashboardRoutes = loadable(() => import('../../layouts/JKDashboardRoutes')); @@ -233,6 +234,7 @@ function JKDashboardMain() { + {/*Redirect*/} diff --git a/jam-ui/src/components/page/JKEditAccount.js b/jam-ui/src/components/page/JKEditAccount.js new file mode 100644 index 000000000..82d2ac005 --- /dev/null +++ b/jam-ui/src/components/page/JKEditAccount.js @@ -0,0 +1,46 @@ +import React, { useState } from 'react'; +import { Row, Col, Card, CardBody } from 'reactstrap'; +import FalconCardHeader from '../common/FalconCardHeader'; +import { useTranslation } from 'react-i18next'; + +import JKEditEmail from '../profile/JKEditEmail'; +import JKEditPassword from '../profile/JKEditPassword'; +import JKModalDialog from '../common/JKModalDialog'; + +const JKEditAccount = () => { + const { t } = useTranslation('account'); + const [alertText, setAlertText] = useState("") + + const [showAlert, setShowAlert] = useState(false); + const toggleAlert = () => setShowAlert(!showAlert); + + return ( + <> + + + + + + + + + + + + + + + + {alertText} + + + ); +}; + +export default JKEditAccount; diff --git a/jam-ui/src/components/profile/JKEditEmail.js b/jam-ui/src/components/profile/JKEditEmail.js new file mode 100644 index 000000000..296ead950 --- /dev/null +++ b/jam-ui/src/components/profile/JKEditEmail.js @@ -0,0 +1,165 @@ +import React, { useState, useEffect } from 'react'; +import { + + Card, + CardHeader, + CardBody, + Form, + FormGroup, + Label, + Input, + InputGroup, + InputGroupAddon +} from 'reactstrap'; + +import { useTranslation } from 'react-i18next'; +import { useAuth } from '../../context/UserAuth'; +import { useForm, Controller } from 'react-hook-form'; +import { postUpdateAccountEmail } from '../../helpers/rest'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + +const JKEditEmail = ({setAlert, toggleAlert}) => { + const { t } = useTranslation('account'); + const { currentUser } = useAuth(); + const [user, setUser] = useState(null); + const [showEmailPassword, setShowEmailPassword] = useState(false); + const [submitting, setSubmitting] = useState(false) + + const { + handleSubmit, + control, + formState: { errors }, + setError, + setValue, + } = useForm({ + defaultValues: { + current_password: '', + new_email: '' + } + }); + + useEffect(() => { + if (currentUser) { + setUser(currentUser); + } + }, [currentUser]); + + const onSubmitEmail = (data) => { + setSubmitting(true) + //post + const { new_email, current_password } = data; + postUpdateAccountEmail(currentUser.id, { email: new_email, current_password }) + .then(response => { + setAlert('A confirmation email has been sent to your email. Please check your email and click the link to confirm your new email address.'); + setValue('current_password', ''); + setValue('new_email', ''); + toggleAlert() + }) + .catch(async error => { + const errorResp = await error.json() + console.log(errorResp) + if(errorResp.errors){ + const errors = errorResp.errors; + if(errors.current_password && errors.current_password.length){ + errors.current_password.forEach(error => { + setError('current_password', { + type: 'manual', + message: `Current password ${error}` + }) + }) + } + if(errors.update_email && errors.update_email.length){ + errors.update_email.forEach(error => { + setError('new_email', { + type: 'manual', + message: `New email ${error}` + }) + }) + } + } + + }).finally(() => { + setSubmitting(false) + }) + }; + + + return ( + + +
Email
+
+ + + To update the email associated with your account, enter your current password (for security reasons) and the + new email, and click the "Save Email" button. + + +
+ + + ( + + + { + setShowEmailPassword(!showEmailPassword); + }} + > + + + + + + )} + /> + {errors.current_password &&
{errors.current_password.message}
} +
+ + + ( + + )} + /> + {errors.new_email &&
{errors.new_email.message}
} +
+
+ + + { submitting && } + +
+
+
+
+ ); +}; + +export default JKEditEmail; diff --git a/jam-ui/src/components/profile/JKEditPassword.js b/jam-ui/src/components/profile/JKEditPassword.js new file mode 100644 index 000000000..55f199d81 --- /dev/null +++ b/jam-ui/src/components/profile/JKEditPassword.js @@ -0,0 +1,191 @@ +import React, { useState, useEffect } from 'react'; +import { Card, CardHeader, CardBody, Form, FormGroup, Label, Input, InputGroup, InputGroupAddon } from 'reactstrap'; + +import { useTranslation } from 'react-i18next'; +import { useAuth } from '../../context/UserAuth'; +import { useForm, Controller } from 'react-hook-form'; +import { postUpdateAccountPassword, requestPasswordReset } from '../../helpers/rest'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + +const JKEditPassword = ({ setAlert, toggleAlert }) => { + const { t } = useTranslation('account'); + const { currentUser } = useAuth(); + const [user, setUser] = useState(null); + + const [showPassword, setShowPassword] = useState(false); + const [showNewPassword, setShowNewPassword] = useState(false); + const [submitting, setSubmitting] = useState(false); + + const { + handleSubmit: handleSubmit, + control: control, + formState: { errors }, + setError, + setValue + } = useForm({ + defaultValues: { + current_password: '', + new_password: '' + } + }); + + useEffect(() => { + if (currentUser) { + setUser(currentUser); + } + }, [currentUser]); + + const onSubmitPassword = data => { + setSubmitting(true); + const { new_password, current_password } = data; + postUpdateAccountPassword(currentUser.id, { current_password, new_password }) + .then(response => { + setAlert('Your password has been successfully updated.'); + setValue('current_password', ''); + setValue('new_password', ''); + toggleAlert(); + }) + .catch(async error => { + console.log(error); + const errorResp = await error.json(); + console.log(errorResp); + if (errorResp.errors) { + const errors = errorResp.errors; + if (errors.current_password && errors.current_password.length) { + errors.current_password.forEach(error => { + setError('current_password', { + type: 'manual', + message: `Current password ${error}` + }); + }); + } + if (errors.password && errors.password.length) { + errors.password.forEach(error => { + setError('new_password', { + type: 'manual', + message: `New password ${error}` + }); + }); + } + } + }) + .finally(() => { + setSubmitting(false); + }); + }; + + const requestResetPassword = async (e) => { + e.preventDefault() + if (!currentUser) { + return; + } + try { + await requestPasswordReset(currentUser.id); + setAlert( + 'A password reset email has been sent to your email. Please check your email and click the link to reset your password.' + ); + toggleAlert(); + } catch (error) { + console.log(error); + } + }; + + return ( + + +
Password
+
+ + + To update the password associated with your account, enter your current password (for security reasons) and + the new password, and click the "Save Password" button. + + +
+ + + ( + + + { + setShowPassword(!showPassword); + }} + > + + + + + + )} + /> + {errors.current_password && ( +
+ {errors.current_password.message} +
+ )} +
+ + + + ( + + + { + setShowNewPassword(!showNewPassword); + }} + > + + + + + + )} + /> + {errors.new_password && ( +
+ {errors.new_password.message} +
+ )} +
+
+ + {submitting && } +
+
+
+ + If you can not remember your current password, Click here to reset + your password, and you will receive an email with instructions on how to reset your password to the email + address associated with your JamKazam account. + +
+
+
+ ); +}; + +export default JKEditPassword; diff --git a/jam-ui/src/helpers/apiFetch.js b/jam-ui/src/helpers/apiFetch.js index 611501524..3070e0a66 100644 --- a/jam-ui/src/helpers/apiFetch.js +++ b/jam-ui/src/helpers/apiFetch.js @@ -41,7 +41,6 @@ function secureFetch(path, options) { break; default: console.log('apiFetch Some error occured'); - break; } //here you also can thorow custom error too reject(response); diff --git a/jam-ui/src/helpers/initFA.js b/jam-ui/src/helpers/initFA.js index 9f5c0bdc3..3c3f94fa5 100644 --- a/jam-ui/src/helpers/initFA.js +++ b/jam-ui/src/helpers/initFA.js @@ -87,6 +87,7 @@ import { faExclamationTriangle, faExternalLinkAlt, faEye, + faEyeSlash, faFileAlt, faFileArchive, faFilePdf, @@ -158,7 +159,7 @@ import { faRecordVinyl, faAddressCard, faVolumeUp, - faSpinner + faSpinner, } from '@fortawesome/free-solid-svg-icons'; //import { faAcousticGuitar } from "../icons"; @@ -245,6 +246,7 @@ library.add( faFilePdf, faFileAlt, faEye, + faEyeSlash, faCaretUp, faCodeBranch, faExclamationTriangle, diff --git a/jam-ui/src/helpers/rest.js b/jam-ui/src/helpers/rest.js index a3b781beb..4a0655aac 100644 --- a/jam-ui/src/helpers/rest.js +++ b/jam-ui/src/helpers/rest.js @@ -1,12 +1,12 @@ -import apiFetch from "./apiFetch"; +import apiFetch from './apiFetch'; -export const getMusicians = (page) => { +export const getMusicians = page => { return new Promise((resolve, reject) => { apiFetch(`/search/musicians?results=true`) - .then(response => resolve(response)) - .catch(error => reject(error)) - }) -} + .then(response => resolve(response)) + .catch(error => reject(error)); + }); +}; // export const getPeople = (page) => { // return new Promise((resolve, reject) => { @@ -16,32 +16,32 @@ export const getMusicians = (page) => { // }) // } -export const getPersonById = (id) => { - return new Promise((resolve, reject) => ( +export const getPersonById = id => { + return new Promise((resolve, reject) => apiFetch(`/users/${id}/profile?show_teacher=true`) - .then(response => resolve(response)) - .catch(error => reject(error)) - )) -} + .then(response => resolve(response)) + .catch(error => reject(error)) + ); +}; export const getPeople = ({ data, offset, limit } = {}) => { return new Promise((resolve, reject) => { apiFetch(`/filter?offset=${offset}&limit=${limit}`, { method: 'POST', - body: JSON.stringify(data) + body: JSON.stringify(data) }) - .then(response => resolve(response)) - .catch(error => reject(error)) - }) -} + .then(response => resolve(response)) + .catch(error => reject(error)); + }); +}; export const getPeopleIndex = () => { return new Promise((resolve, reject) => { apiFetch(`/users`) - .then(response => resolve(response)) - .catch(error => reject(error)) - }) -} + .then(response => resolve(response)) + .catch(error => reject(error)); + }); +}; export const getLobbyUsers = () => { return new Promise((resolve, reject) => { @@ -65,18 +65,18 @@ export const updateUser = (id, data) => { export const getGenres = () => { return new Promise((resolve, reject) => { apiFetch('/genres') - .then(response => resolve(response)) - .catch(error => reject(error)) - }) -} + .then(response => resolve(response)) + .catch(error => reject(error)); + }); +}; export const getInstruments = () => { return new Promise((resolve, reject) => { apiFetch('/instruments') - .then(response => resolve(response)) - .catch(error => reject(error)) - }) -} + .then(response => resolve(response)) + .catch(error => reject(error)); + }); +}; // export const getCurrentUser = () => { // return new Promise((resolve, reject) => { @@ -86,13 +86,13 @@ export const getInstruments = () => { // }) // } -export const getFriends = (userId) => { +export const getFriends = userId => { return new Promise((resolve, reject) => { apiFetch(`/users/${userId}/friends`) - .then(response => resolve(response)) - .catch(error => reject(error)) - }) -} + .then(response => resolve(response)) + .catch(error => reject(error)); + }); +}; export const addFriend = (userId, friendId) => { return new Promise((resolve, reject) => { @@ -100,108 +100,106 @@ export const addFriend = (userId, friendId) => { method: 'POST', body: JSON.stringify({ friend_id: friendId }) }) - .then(response => resolve(response)) - .catch(error => reject(error)) - }) -} + .then(response => resolve(response)) + .catch(error => reject(error)); + }); +}; export const removeFriend = (userId, friendId) => { return new Promise((resolve, reject) => { apiFetch(`/users/${userId}/friends/${friendId}`, { method: 'DELETE' }) - .then(response => resolve(response)) - .catch(error => reject(error)) - }) -} + .then(response => resolve(response)) + .catch(error => reject(error)); + }); +}; export const getTextMessages = (options = {}) => { return new Promise((resolve, reject) => { apiFetch(`/text_messages?${new URLSearchParams(options)}`) - .then(response => resolve(response)) - .catch(error => reject(error)) - }) -} + .then(response => resolve(response)) + .catch(error => reject(error)); + }); +}; -export const createTextMessage = (options) => { +export const createTextMessage = options => { return new Promise((resolve, reject) => { apiFetch(`/text_messages`, { - method: "POST", + method: 'POST', body: JSON.stringify(options) }) - .then(response => resolve(response)) - .catch(error => reject(error)) - }) -} + .then(response => resolve(response)) + .catch(error => reject(error)); + }); +}; -export const createLobbyChatMessage = (options) => { +export const createLobbyChatMessage = options => { return new Promise((resolve, reject) => { apiFetch(`/chat`, { - method: "POST", + method: 'POST', body: JSON.stringify(options) }) - .then(response => resolve(response)) - .catch(error => reject(error)) - }) -} + .then(response => resolve(response)) + .catch(error => reject(error)); + }); +}; export const getNotifications = (userId, options = {}) => { return new Promise((resolve, reject) => { apiFetch(`/users/${userId}/notifications?${new URLSearchParams(options)}`) - .then(response => resolve(response)) - .catch(error => reject(error)) - }) - -} + .then(response => resolve(response)) + .catch(error => reject(error)); + }); +}; export const acceptFriendRequest = (userId, options = {}) => { return new Promise((resolve, reject) => { - const { status, friend_request_id } = options + const { status, friend_request_id } = options; apiFetch(`/users/${userId}/friend_requests/${friend_request_id}`, { method: 'POST', body: JSON.stringify({ status }) }) - .then(response => resolve(response)) - .catch(error => reject(error)) - }) -} + .then(response => resolve(response)) + .catch(error => reject(error)); + }); +}; export const deleteNotification = (userId, notificationId) => { return new Promise((resolve, reject) => { apiFetch(`/users/${userId}/notifications/${notificationId}`, { - method: 'DELETE', + method: 'DELETE' }) - .then(response => resolve(response)) - .catch(error => reject(error)) - }) -} + .then(response => resolve(response)) + .catch(error => reject(error)); + }); +}; export const getSessions = () => { return new Promise((resolve, reject) => { apiFetch(`/sessions`) - .then(response => resolve(response)) - .catch(error => reject(error)) - }) -} + .then(response => resolve(response)) + .catch(error => reject(error)); + }); +}; export const getLatencyToUsers = (currentUserId, participantIds) => { return new Promise((resolve, reject) => { - const query = participantIds.map(id => `user_ids[]=${id}`).join('&') + const query = participantIds.map(id => `user_ids[]=${id}`).join('&'); apiFetch(`/users/${currentUserId}/latencies?${query}`) - .then(response => resolve(response)) - .catch(error => reject(error)) - }) -} + .then(response => resolve(response)) + .catch(error => reject(error)); + }); +}; export const getLobbyChatMessages = (options = {}) => { return new Promise((resolve, reject) => { - console.log('getLobbyChatMessages', options) + console.log('getLobbyChatMessages', options); apiFetch(`/chat?${new URLSearchParams(options)}`) - .then(response => resolve(response)) - .catch(error => reject(error)) - }) -} - + .then(response => resolve(response)) + .catch(error => reject(error)); + }); +}; export const updateUser = (userId, options) => { return new Promise((resolve, reject) => { @@ -209,32 +207,65 @@ export const updateUser = (userId, options) => { method: 'PATCH', body: JSON.stringify(options) }) - .then(response => resolve(response)) - .catch(error => reject(error)) - }) -} - + .then(response => resolve(response)) + .catch(error => reject(error)); + }); +}; export const getCountries = () => { return new Promise((resolve, reject) => { apiFetch(`/countries`) - .then(response => resolve(response)) - .catch(error => reject(error)) - }) -} + .then(response => resolve(response)) + .catch(error => reject(error)); + }); +}; -export const getRegions = (countryId) => { +export const getRegions = countryId => { return new Promise((resolve, reject) => { apiFetch(`/regions?country=${countryId}`) - .then(response => resolve(response)) - .catch(error => reject(error)) - }) -} + .then(response => resolve(response)) + .catch(error => reject(error)); + }); +}; export const getCities = (countryId, regionId) => { return new Promise((resolve, reject) => { apiFetch(`/cities?country=${countryId}®ion=${regionId}`) - .then(response => resolve(response)) - .catch(error => reject(error)) - }) + .then(response => resolve(response)) + .catch(error => reject(error)); + }); +}; + +export const postUpdateAccountEmail = (userId, options) => { + const { email, current_password } = options; + return new Promise((resolve, reject) => { + apiFetch(`/users/${userId}/update_email`, { + method: 'POST', + body: JSON.stringify({ update_email: email, current_password }) + }) + .then(response => resolve(response)) + .catch(error => reject(error)); + }); +}; + +export const postUpdateAccountPassword = (userId, options) => { + const { new_password, current_password } = options; + return new Promise((resolve, reject) => { + apiFetch(`/users/${userId}/set_password`, { + method: 'POST', + body: JSON.stringify({ old_password: current_password, new_password, new_password_confirm: new_password }) + }) + .then(response => resolve(response)) + .catch(error => reject(error)); + }); +}; + +export const requestPasswordReset = (userId) => { + return new Promise((resolve, reject) => { + apiFetch(`/users/${userId}/request_reset_password`, { + method: 'POST', + }) + .then(response => resolve(response)) + .catch(error => reject(error)); + }); } \ No newline at end of file diff --git a/jam-ui/src/i18n/config.js b/jam-ui/src/i18n/config.js index 56c791242..35dfabd95 100644 --- a/jam-ui/src/i18n/config.js +++ b/jam-ui/src/i18n/config.js @@ -8,6 +8,7 @@ import authTranslationsEN from './locales/en/auth.json' import sessTranslationsEN from './locales/en/sessions.json' import unsubscribeTranslationsEN from './locales/en/unsubscribe.json' import profileEN from './locales/en/profile.json' +import accountEN from './locales/en/account.json' import commonTranslationsES from './locales/es/common.json' import homeTranslationsES from './locales/es/home.json' @@ -16,6 +17,7 @@ import authTranslationsES from './locales/es/auth.json' import sessTranslationsES from './locales/es/sessions.json' import unsubscribeTranslationsES from './locales/es/unsubscribe.json' import profileES from './locales/es/profile.json' +import accountES from './locales/es/account.json' i18n.use(initReactI18next).init({ fallbackLng: 'en', @@ -29,7 +31,8 @@ i18n.use(initReactI18next).init({ auth: authTranslationsEN, sessions: sessTranslationsEN, unsubscribe: unsubscribeTranslationsEN, - profile: profileEN + profile: profileEN, + account: accountEN }, es: { //translations: require('./locales/es/translations.json') @@ -39,7 +42,8 @@ i18n.use(initReactI18next).init({ auth: authTranslationsES, sessions: sessTranslationsES, unsubscribe: unsubscribeTranslationsES, - profile: profileES + profile: profileES, + account: accountES } }, //ns: ['translations'], diff --git a/jam-ui/src/i18n/locales/en/account.json b/jam-ui/src/i18n/locales/en/account.json new file mode 100644 index 000000000..a78f96aae --- /dev/null +++ b/jam-ui/src/i18n/locales/en/account.json @@ -0,0 +1,11 @@ +{ + "identity": { + "page_title": "Identity", + "modals": { + "update_notification": { + "title": "Account Identity Updated" + } + } + + } +} \ No newline at end of file diff --git a/jam-ui/src/i18n/locales/es/account.json b/jam-ui/src/i18n/locales/es/account.json new file mode 100644 index 000000000..2ddff10c2 --- /dev/null +++ b/jam-ui/src/i18n/locales/es/account.json @@ -0,0 +1,4 @@ + +{ + +} \ No newline at end of file diff --git a/web/app/controllers/api_users_controller.rb b/web/app/controllers/api_users_controller.rb index 5173d7ff6..499e353b4 100644 --- a/web/app/controllers/api_users_controller.rb +++ b/web/app/controllers/api_users_controller.rb @@ -12,7 +12,7 @@ class ApiUsersController < ApiController :friend_show, :friend_destroy, # friends :notification_index, :notification_destroy, # notifications :band_invitation_index, :band_invitation_show, :band_invitation_update, # band invitations - :set_password, :begin_update_email, :update_avatar, :delete_avatar, :generate_filepicker_policy, + :set_password, :begin_update_email, :update_avatar, :delete_avatar, :generate_filepicker_policy, :request_reset_password, :share_session, :share_recording, :affiliate_report, :audio_latency, :get_latencies, :broadcast_notification, :redeem_giftcard] @@ -339,6 +339,11 @@ class ApiUsersController < ApiController respond_with responder: ApiResponder, :status => 204 end + def request_reset_password + User.reset_password(current_user.email, ApplicationHelper.base_uri(request)) + respond_with current_user, responder: ApiResponder, :status => 200 + end + ###################### AUTHENTICATION ################### def auth_session_create @user = User.authenticate(params[:email], params[:password]) diff --git a/web/config/routes.rb b/web/config/routes.rb index d228494d3..477612d90 100644 --- a/web/config/routes.rb +++ b/web/config/routes.rb @@ -404,6 +404,7 @@ Rails.application.routes.draw do match '/users/complete/:signup_token' => 'api_users#complete', as: 'complete', via: 'post' match '/users/authorizations/google' => 'api_users#google_auth', :via => :get match '/users/:id/set_password' => 'api_users#set_password', :via => :post + match '/users/:id/request_reset_password' => 'api_users#request_reset_password', :via => :post match '/reviews' => 'api_reviews#index', :via => :get match '/reviews' => 'api_reviews#create', :via => :post