account identity update

allow users to update their JamKazam email and password securly.
This commit is contained in:
Nuwan 2024-02-03 18:29:57 +05:30
parent fe6c372d3d
commit dd4239f1f3
13 changed files with 574 additions and 112 deletions

View File

@ -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 (
<Modal isOpen={modal} toggle={toggle}>
<Modal isOpen={modal} toggle={toggle} {...rest}>
<ModalHeader toggle={toggle}>{title}</ModalHeader>
<ModalBody>{children}</ModalBody>
{showFooter && (

View File

@ -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() {
<PrivateRoute path="/sessions" component={JKMusicSessions} />
<PrivateRoute path="/notifications" component={JKNotifications} />
<PrivateRoute path="/profile" component={JKEditProfile} />
<PrivateRoute path="/account/identity" component={JKEditAccount} />
{/*Redirect*/}
<Redirect to="/errors/404" />
</Switch>

View File

@ -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 (
<>
<Card>
<FalconCardHeader title={t('identity.page_title')} titleClass="font-weight-bold" />
<CardBody className="pt-3" style={{ backgroundColor: '#edf2f9' }}>
<Row>
<Col>
<JKEditEmail setAlert={setAlertText} toggleAlert={toggleAlert} />
</Col>
<Col>
<JKEditPassword setAlert={setAlertText} toggleAlert={toggleAlert} />
</Col>
<Col></Col>
</Row>
</CardBody>
</Card>
<JKModalDialog
show={showAlert}
onToggle={toggleAlert}
title={t('identity.modals.update_notification.title')}
data-testid="native-app-unavailable"
size="md"
>
{alertText}
</JKModalDialog>
</>
);
};
export default JKEditAccount;

View File

@ -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 (
<Card>
<CardHeader>
<h5>Email</h5>
</CardHeader>
<CardBody className="bg-light" style={{ minHeight: 300 }}>
<small>
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.
</small>
<Form className="mt-2" onSubmit={handleSubmit(onSubmitEmail)}>
<FormGroup>
<Label for="current_password">Current Password</Label>
<Controller
name="current_password"
control={control}
rules={{ required: 'Current password is required' }}
render={({ field }) => (
<InputGroup>
<Input
{...field}
type={showEmailPassword ? 'text' : 'password'}
className="form-control"
id="current_password"
placeholder="Current Password"
/>
<InputGroupAddon
addonType="append"
onClick={() => {
setShowEmailPassword(!showEmailPassword);
}}
>
<span className="input-group-text">
<FontAwesomeIcon icon={showEmailPassword ? 'eye-slash' : 'eye'} />
</span>
</InputGroupAddon>
</InputGroup>
)}
/>
{errors.current_password && <div className="text-danger"><small>{errors.current_password.message}</small></div>}
</FormGroup>
<FormGroup>
<Label for="new_email">New Email</Label>
<Controller
name="new_email"
control={control}
rules={{
required: 'New email is required',
pattern: {
value: /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$/,
message: 'Invalid email address'
}
}}
render={({ field }) => (
<Input
{...field}
type="email"
className="form-control"
id="new_email"
placeholder={user ? user.email : 'New Email'}
/>
)}
/>
{errors.new_email && <div className="text-danger"><small>{errors.new_email.message}</small></div>}
</FormGroup>
<div className='d-flex align-content-center justify-content-start'>
<input type="submit" className="btn btn-primary" value="Save Email" disabled={submitting} />
<span className='ml-2'>
{ submitting && <FontAwesomeIcon icon="spinner" />}
</span>
</div>
</Form>
</CardBody>
</Card>
);
};
export default JKEditEmail;

View File

@ -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 (
<Card>
<CardHeader>
<h5>Password</h5>
</CardHeader>
<CardBody className="bg-light" style={{ minHeight: 300 }}>
<small>
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.
</small>
<Form className="mt-2" onSubmit={handleSubmit(onSubmitPassword)}>
<FormGroup>
<Label for="current_password">Current Password</Label>
<Controller
name="current_password"
control={control}
rules={{ required: 'Current password is required' }}
render={({ field }) => (
<InputGroup>
<Input
{...field}
type={showPassword ? 'text' : 'password'}
className="form-control"
id="current_password"
placeholder="Current Password"
/>
<InputGroupAddon
addonType="append"
onClick={() => {
setShowPassword(!showPassword);
}}
>
<span className="input-group-text">
<FontAwesomeIcon icon={showPassword ? 'eye-slash' : 'eye'} />
</span>
</InputGroupAddon>
</InputGroup>
)}
/>
{errors.current_password && (
<div className="text-danger">
<small>{errors.current_password.message}</small>
</div>
)}
</FormGroup>
<FormGroup>
<Label for="new_password">New Password</Label>
<Controller
name="new_password"
control={control}
rules={{ required: 'New password is required' }}
render={({ field }) => (
<InputGroup>
<Input
{...field}
type={showNewPassword ? 'text' : 'password'}
className="form-control"
id="new_password"
placeholder="New Password"
/>
<InputGroupAddon
addonType="append"
onClick={() => {
setShowNewPassword(!showNewPassword);
}}
>
<span className="input-group-text">
<FontAwesomeIcon icon={showNewPassword ? 'eye-slash' : 'eye'} />
</span>
</InputGroupAddon>
</InputGroup>
)}
/>
{errors.new_password && (
<div className="text-danger">
<small>{errors.new_password.message}</small>
</div>
)}
</FormGroup>
<div className="d-flex align-content-center justify-content-start">
<input type="submit" className="btn btn-primary" value="Save Password" disabled={submitting} />
<span className="ml-2">{submitting && <FontAwesomeIcon icon="spinner" />}</span>
</div>
</Form>
<div className="mt-2">
<small>
If you can not remember your current password, <a href="#" onClick={requestResetPassword}>Click here</a> 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.
</small>
</div>
</CardBody>
</Card>
);
};
export default JKEditPassword;

View File

@ -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);

View File

@ -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,

View File

@ -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}&region=${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));
});
}

View File

@ -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'],

View File

@ -0,0 +1,11 @@
{
"identity": {
"page_title": "Identity",
"modals": {
"update_notification": {
"title": "Account Identity Updated"
}
}
}
}

View File

@ -0,0 +1,4 @@
{
}

View File

@ -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])

View File

@ -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