mirror of
https://github.com/wassname/talk.git
synced 2026-06-30 09:24:53 +08:00
Merge branch 'master' into fix-cli-user-create
This commit is contained in:
@@ -71,7 +71,6 @@ class OrganizationSettings extends React.Component {
|
||||
await this.props.savePending();
|
||||
this.disableEditing();
|
||||
};
|
||||
|
||||
displayErrors = (errors = []) => (
|
||||
<ul className={styles.errorList}>
|
||||
{errors.map((errKey, i) => (
|
||||
|
||||
@@ -25,6 +25,7 @@ export default {
|
||||
'UnsuspendUserResponse',
|
||||
'UpdateAssetSettingsResponse',
|
||||
'UpdateAssetStatusResponse',
|
||||
'UpdateSettingsResponse'
|
||||
'UpdateSettingsResponse',
|
||||
'ChangePasswordResponse'
|
||||
),
|
||||
};
|
||||
|
||||
@@ -623,6 +623,27 @@ export const withUpdateSettings = withMutation(
|
||||
}
|
||||
);
|
||||
|
||||
export const withChangePassword = withMutation(
|
||||
gql`
|
||||
mutation ChangePassword($input: ChangePasswordInput!) {
|
||||
changePassword(input: $input) {
|
||||
...ChangePasswordResponse
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
props: ({ mutate }) => ({
|
||||
changePassword: input => {
|
||||
return mutate({
|
||||
variables: {
|
||||
input,
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
export const withUpdateAssetSettings = withMutation(
|
||||
gql`
|
||||
mutation UpdateAssetSettings($id: ID!, $input: AssetSettingsInput!) {
|
||||
|
||||
@@ -9,6 +9,7 @@ const {
|
||||
SET_USER_SUSPENSION_STATUS,
|
||||
UPDATE_USER_ROLES,
|
||||
DELETE_USER,
|
||||
CHANGE_PASSWORD,
|
||||
} = require('../../perms/constants');
|
||||
|
||||
const setUserUsernameStatus = async (ctx, id, status) => {
|
||||
@@ -143,6 +144,38 @@ const delUser = async (ctx, id) => {
|
||||
await user.remove();
|
||||
};
|
||||
|
||||
const changeUserPassword = async (ctx, oldPassword, newPassword) => {
|
||||
const {
|
||||
user,
|
||||
loaders: { Settings },
|
||||
connectors: { services: { I18n } },
|
||||
} = ctx;
|
||||
|
||||
// Verify the old password.
|
||||
const validPassword = await user.verifyPassword(oldPassword);
|
||||
if (!validPassword) {
|
||||
throw new ErrNotAuthorized();
|
||||
}
|
||||
|
||||
// Change the users password now.
|
||||
await Users.changePassword(user.id, newPassword);
|
||||
|
||||
// Get some context for the email to be sent.
|
||||
const { organizationName, organizationContactEmail } = await Settings.load([
|
||||
'organizationName',
|
||||
'organizationContactEmail',
|
||||
]);
|
||||
|
||||
// Send the password change email.
|
||||
await Users.sendEmail(user, {
|
||||
template: 'plain',
|
||||
locals: {
|
||||
body: I18n.t('email.password_change.body', organizationContactEmail),
|
||||
},
|
||||
subject: I18n.t('email.password_change.subject', organizationName),
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = ctx => {
|
||||
let mutators = {
|
||||
User: {
|
||||
@@ -155,6 +188,7 @@ module.exports = ctx => {
|
||||
setUsername: () => Promise.reject(new ErrNotAuthorized()),
|
||||
stopIgnoringUser: () => Promise.reject(new ErrNotAuthorized()),
|
||||
del: () => Promise.reject(new ErrNotAuthorized()),
|
||||
changePassword: () => Promise.reject(new ErrNotAuthorized()),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -194,6 +228,11 @@ module.exports = ctx => {
|
||||
if (ctx.user.can(DELETE_USER)) {
|
||||
mutators.User.del = id => delUser(ctx, id);
|
||||
}
|
||||
|
||||
if (ctx.user.can(CHANGE_PASSWORD)) {
|
||||
mutators.User.changePassword = ({ oldPassword, newPassword }) =>
|
||||
changeUserPassword(ctx, oldPassword, newPassword);
|
||||
}
|
||||
}
|
||||
|
||||
return mutators;
|
||||
|
||||
@@ -139,6 +139,9 @@ const RootMutation = {
|
||||
delUser: async (_, { id }, { mutators: { User } }) => {
|
||||
await User.del(id);
|
||||
},
|
||||
changePassword: async (_, { input }, { mutators: { User } }) => {
|
||||
await User.changePassword(input);
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = RootMutation;
|
||||
|
||||
@@ -1442,6 +1442,21 @@ type DelUserResponse implements Response {
|
||||
errors: [UserError!]
|
||||
}
|
||||
|
||||
input ChangePasswordInput {
|
||||
# oldPassword is the previous password set on the account. An incorrect
|
||||
# password here will result in an unauthorized error being thrown.
|
||||
oldPassword: String!
|
||||
|
||||
# newPassword is the password we're changing it to.
|
||||
newPassword: String!
|
||||
}
|
||||
|
||||
type ChangePasswordResponse implements Response {
|
||||
|
||||
# An array of errors relating to the mutation that occurred.
|
||||
errors: [UserError!]
|
||||
}
|
||||
|
||||
# All mutations for the application are defined on this object.
|
||||
type RootMutation {
|
||||
|
||||
@@ -1542,6 +1557,10 @@ type RootMutation {
|
||||
|
||||
# delUser will delete the user with the specified id.
|
||||
delUser(id: ID!): DelUserResponse
|
||||
|
||||
# changePassword allows the current user to change their password that have an
|
||||
# associated local user account.
|
||||
changePassword(input: ChangePasswordInput!): ChangePasswordResponse
|
||||
}
|
||||
|
||||
type UsernameChangedPayload {
|
||||
|
||||
@@ -218,6 +218,9 @@ en:
|
||||
we_received_a_request: "We received a request to reset your password. If you did not request this change, you can ignore this email."
|
||||
if_you_did: "If you did,"
|
||||
please_click: "please click here to reset password"
|
||||
password_change:
|
||||
subject: "{0} password change"
|
||||
body: "The password on your account has been changed.\n\nIf you did not request this change, please contact us at {0}."
|
||||
embedlink:
|
||||
copy: "Copy to Clipboard"
|
||||
error:
|
||||
|
||||
@@ -19,4 +19,5 @@ module.exports = {
|
||||
UPDATE_ASSET_STATUS: 'UPDATE_ASSET_STATUS',
|
||||
UPDATE_SETTINGS: 'UPDATE_SETTINGS',
|
||||
DELETE_USER: 'DELETE_USER',
|
||||
CHANGE_PASSWORD: 'CHANGE_PASSWORD',
|
||||
};
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
const { isString } = require('lodash');
|
||||
const { check } = require('../utils');
|
||||
const types = require('../constants');
|
||||
|
||||
module.exports = (user, perm) => {
|
||||
switch (perm) {
|
||||
case types.CHANGE_PASSWORD:
|
||||
// Only users with a local account where they have a password set can
|
||||
// actually change their password.
|
||||
return (
|
||||
user.profiles.some(({ provider }) => provider === 'local') &&
|
||||
isString(user.password) &&
|
||||
user.password.length > 0
|
||||
);
|
||||
case types.CHANGE_USERNAME:
|
||||
return user.status.username.status === 'REJECTED';
|
||||
|
||||
|
||||
@@ -25,5 +25,6 @@ export {
|
||||
withUnbanUser,
|
||||
withStopIgnoringUser,
|
||||
withSetCommentStatus,
|
||||
withChangePassword,
|
||||
} from 'coral-framework/graphql/mutations';
|
||||
export { compose } from 'recompose';
|
||||
|
||||
@@ -4,6 +4,7 @@ import SetUsernameDialog from './stream/containers/SetUsernameDialog';
|
||||
import translations from './translations.yml';
|
||||
import Login from './login/containers/Main';
|
||||
import reducer from './login/reducer';
|
||||
import ChangePassword from './profile-settings/containers/ChangePassword';
|
||||
|
||||
export default {
|
||||
reducer,
|
||||
@@ -11,5 +12,6 @@ export default {
|
||||
slots: {
|
||||
stream: [UserBox, SignInButton, SetUsernameDialog],
|
||||
login: [Login],
|
||||
profileSettings: [ChangePassword],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
.container {
|
||||
position: relative;
|
||||
color: #202020;
|
||||
padding: 10px;
|
||||
border-radius: 2px;
|
||||
border: solid 1px transparent;
|
||||
box-sizing: border-box;
|
||||
justify-content: space-between;
|
||||
|
||||
&.editing {
|
||||
border-color: #979797;
|
||||
background-color: #EDEDED;
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: #202020;
|
||||
margin: 0 0 20px;
|
||||
}
|
||||
|
||||
.detailBottomBox {
|
||||
display: block;
|
||||
padding-top: 4px;
|
||||
text-align: right;
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
.detailLink {
|
||||
color: #00538A;
|
||||
text-decoration: none;
|
||||
font-size: 0.9em;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
border: 1px solid #787d80;
|
||||
background-color: transparent;
|
||||
height: 30px;
|
||||
font-size: 1em;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.saveButton {
|
||||
background-color: #3498DB;
|
||||
border-color: #3498DB;
|
||||
color: white;
|
||||
|
||||
> i {
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #399ee2;
|
||||
color: white;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
border-color: #e0e0e0;
|
||||
|
||||
&:hover {
|
||||
background-color: #e0e0e0;
|
||||
color: #4f5c67;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cancelButton {
|
||||
color:#787D80;
|
||||
margin-top: 6px;
|
||||
font-size: 0.9em;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cn from 'classnames';
|
||||
import styles from './ChangePassword.css';
|
||||
import { Button } from 'plugin-api/beta/client/components/ui';
|
||||
import validate from 'coral-framework/helpers/validate';
|
||||
import errorMsj from 'coral-framework/helpers/error';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import { t } from 'plugin-api/beta/client/services';
|
||||
import Form from './Form';
|
||||
import InputField from './InputField';
|
||||
import { getErrorMessages } from 'coral-framework/utils';
|
||||
|
||||
const initialState = {
|
||||
editing: false,
|
||||
showErrors: true,
|
||||
errors: {},
|
||||
formData: {},
|
||||
};
|
||||
|
||||
class ChangePassword extends React.Component {
|
||||
state = initialState;
|
||||
validKeys = ['oldPassword', 'newPassword', 'confirmNewPassword'];
|
||||
|
||||
onChange = e => {
|
||||
const { name, value, type } = e.target;
|
||||
this.setState(
|
||||
state => ({
|
||||
formData: {
|
||||
...state.formData,
|
||||
[name]: value,
|
||||
},
|
||||
}),
|
||||
() => {
|
||||
this.fieldValidation(value, type, name);
|
||||
|
||||
// Perform equality validation if password fields have changed
|
||||
if (name === 'newPassword' || name === 'confirmNewPassword') {
|
||||
this.equalityValidation('newPassword', 'confirmNewPassword');
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
equalityValidation = (field, field2) => {
|
||||
const cond = this.state.formData[field] === this.state.formData[field2];
|
||||
if (!cond) {
|
||||
this.addError({
|
||||
[field2]: t('talk-plugin-auth.change_password.passwords_dont_match'),
|
||||
});
|
||||
} else {
|
||||
this.removeError(field2);
|
||||
}
|
||||
return cond;
|
||||
};
|
||||
|
||||
fieldValidation = (value, type, name) => {
|
||||
if (!value.length) {
|
||||
this.addError({
|
||||
[name]: t('talk-plugin-auth.change_password.required_field'),
|
||||
});
|
||||
} else if (!validate[type](value)) {
|
||||
this.addError({ [name]: errorMsj[type] });
|
||||
} else {
|
||||
this.removeError(name);
|
||||
}
|
||||
};
|
||||
|
||||
hasError = err => {
|
||||
return Object.keys(this.state.errors).indexOf(err) !== -1;
|
||||
};
|
||||
|
||||
addError = err => {
|
||||
this.setState(({ errors }) => ({
|
||||
errors: { ...errors, ...err },
|
||||
}));
|
||||
};
|
||||
|
||||
removeError = errKey => {
|
||||
this.setState(state => {
|
||||
const { [errKey]: _, ...errors } = state.errors;
|
||||
return {
|
||||
errors,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
enableEditing = () => {
|
||||
this.setState({
|
||||
editing: true,
|
||||
});
|
||||
};
|
||||
|
||||
isSubmitBlocked = () => {
|
||||
const formHasErrors = !!Object.keys(this.state.errors).length;
|
||||
const formIncomplete = !isEqual(
|
||||
Object.keys(this.state.formData),
|
||||
this.validKeys
|
||||
);
|
||||
return formHasErrors || formIncomplete;
|
||||
};
|
||||
|
||||
clearForm = () => {
|
||||
this.setState(initialState);
|
||||
};
|
||||
|
||||
onSave = async () => {
|
||||
const { oldPassword, newPassword } = this.state.formData;
|
||||
|
||||
try {
|
||||
await this.props.changePassword({
|
||||
oldPassword,
|
||||
newPassword,
|
||||
});
|
||||
this.props.notify(
|
||||
'success',
|
||||
t('talk-plugin-auth.change_password.changed_password_msg')
|
||||
);
|
||||
} catch (err) {
|
||||
this.props.notify('error', getErrorMessages(err));
|
||||
}
|
||||
|
||||
this.clearForm();
|
||||
this.disableEditing();
|
||||
};
|
||||
|
||||
disableEditing = () => {
|
||||
this.setState({
|
||||
editing: false,
|
||||
});
|
||||
};
|
||||
|
||||
cancel = () => {
|
||||
this.clearForm();
|
||||
this.disableEditing();
|
||||
};
|
||||
|
||||
render() {
|
||||
const { editing, errors } = this.state;
|
||||
|
||||
return (
|
||||
<section
|
||||
className={cn('talk-plugin-auth--change-password', styles.container, {
|
||||
[styles.editing]: editing,
|
||||
})}
|
||||
>
|
||||
<h3 className={styles.title}>
|
||||
{t('talk-plugin-auth.change_password.change_password')}
|
||||
</h3>
|
||||
{editing && (
|
||||
<Form className="talk-plugin-auth--change-password-form">
|
||||
<InputField
|
||||
id="oldPassword"
|
||||
label="Old Password"
|
||||
name="oldPassword"
|
||||
type="password"
|
||||
onChange={this.onChange}
|
||||
value={this.state.formData.oldPassword}
|
||||
hasError={this.hasError('oldPassword')}
|
||||
errorMsg={errors['oldPassword']}
|
||||
showErrors
|
||||
>
|
||||
<span className={styles.detailBottomBox}>
|
||||
<a className={styles.detailLink}>
|
||||
{t('talk-plugin-auth.change_password.forgot_password')}
|
||||
</a>
|
||||
</span>
|
||||
</InputField>
|
||||
<InputField
|
||||
id="newPassword"
|
||||
label="New Password"
|
||||
name="newPassword"
|
||||
type="password"
|
||||
onChange={this.onChange}
|
||||
value={this.state.formData.newPassword}
|
||||
hasError={this.hasError('newPassword')}
|
||||
errorMsg={errors['newPassword']}
|
||||
showErrors
|
||||
/>
|
||||
<InputField
|
||||
id="confirmNewPassword"
|
||||
label="Confirm New Password"
|
||||
name="confirmNewPassword"
|
||||
type="password"
|
||||
onChange={this.onChange}
|
||||
value={this.state.formData.confirmNewPassword}
|
||||
hasError={this.hasError('confirmNewPassword')}
|
||||
errorMsg={errors['confirmNewPassword']}
|
||||
showErrors
|
||||
/>
|
||||
</Form>
|
||||
)}
|
||||
{editing ? (
|
||||
<div className={styles.actions}>
|
||||
<Button
|
||||
className={cn(styles.button, styles.saveButton)}
|
||||
icon="save"
|
||||
onClick={this.onSave}
|
||||
disabled={this.isSubmitBlocked()}
|
||||
>
|
||||
{t('talk-plugin-auth.change_password.save')}
|
||||
</Button>
|
||||
<a className={styles.cancelButton} onClick={this.cancel}>
|
||||
{t('talk-plugin-auth.change_password.cancel')}
|
||||
</a>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.actions}>
|
||||
<Button className={styles.button} onClick={this.enableEditing}>
|
||||
{t('talk-plugin-auth.change_password.edit')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ChangePassword.propTypes = {
|
||||
changePassword: PropTypes.func.isRequired,
|
||||
notify: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ChangePassword;
|
||||
@@ -0,0 +1,9 @@
|
||||
.errorMsg {
|
||||
color: #FA4643;
|
||||
padding-left: 4px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.warningIcon {
|
||||
color: #FA4643;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './ErrorMessage.css';
|
||||
import { Icon } from 'plugin-api/beta/client/components/ui';
|
||||
|
||||
const ErrorMessage = ({ children }) => (
|
||||
<div className={styles.errorMsg}>
|
||||
<Icon className={styles.warningIcon} name="warning" />
|
||||
<span>{children}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
ErrorMessage.propTypes = {
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
export default ErrorMessage;
|
||||
@@ -0,0 +1,5 @@
|
||||
.detailList {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import styles from './Form.css';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const Form = ({ children, className = '' }) => (
|
||||
<form className={className}>
|
||||
<ul className={styles.detailList}>{children}</ul>
|
||||
</form>
|
||||
);
|
||||
|
||||
Form.propTypes = {
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
export default Form;
|
||||
@@ -0,0 +1,51 @@
|
||||
|
||||
.detailItem {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.detailItemContainer {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.detailItemContent {
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
.detailLabel {
|
||||
color: #4C4C4D;
|
||||
font-size: 1em;
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.detailValue {
|
||||
padding: 6px 2px;
|
||||
border: solid 1px #979797;
|
||||
display: block;
|
||||
font-size: 1.1em;
|
||||
border-radius: 2px;
|
||||
background-color: #ffffff;
|
||||
color: #979797;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.detailItemMessage {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 2px;
|
||||
padding-top: 16px;
|
||||
|
||||
.warningIcon, .checkIcon {
|
||||
font-size: 17px;
|
||||
}
|
||||
}
|
||||
|
||||
.checkIcon {
|
||||
color: #00CD73;
|
||||
}
|
||||
|
||||
.warningIcon {
|
||||
color: #FA4643;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './InputField.css';
|
||||
import ErrorMessage from './ErrorMessage';
|
||||
import { Icon } from 'plugin-api/beta/client/components/ui';
|
||||
|
||||
const InputField = ({
|
||||
id = '',
|
||||
label = '',
|
||||
type = 'text',
|
||||
name = '',
|
||||
onChange = () => {},
|
||||
value = '',
|
||||
showError = true,
|
||||
hasError = false,
|
||||
errorMsg = '',
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<li className={styles.detailItem}>
|
||||
<div className={styles.detailItemContainer}>
|
||||
<div className={styles.detailItemContent}>
|
||||
<label className={styles.detailLabel} id={id}>
|
||||
{label}
|
||||
</label>
|
||||
<input
|
||||
id={id}
|
||||
type={type}
|
||||
name={name}
|
||||
className={styles.detailValue}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.detailItemMessage}>
|
||||
{!hasError &&
|
||||
value && <Icon className={styles.checkIcon} name="check_circle" />}
|
||||
{hasError && showError && <ErrorMessage>{errorMsg}</ErrorMessage>}
|
||||
</div>
|
||||
</div>
|
||||
{children}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
InputField.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func,
|
||||
value: PropTypes.string,
|
||||
showError: PropTypes.bool,
|
||||
hasError: PropTypes.bool,
|
||||
errorMsg: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
export default InputField;
|
||||
@@ -0,0 +1,12 @@
|
||||
import { compose } from 'react-apollo';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'plugin-api/beta/client/hocs';
|
||||
import ChangePassword from '../components/ChangePassword';
|
||||
import { notify } from 'coral-framework/actions/notification';
|
||||
import { withChangePassword } from 'plugin-api/beta/client/hocs';
|
||||
|
||||
const mapDispatchToProps = dispatch => bindActionCreators({ notify }, dispatch);
|
||||
|
||||
export default compose(connect(null, mapDispatchToProps), withChangePassword)(
|
||||
ChangePassword
|
||||
);
|
||||
@@ -131,6 +131,15 @@ en:
|
||||
username: Username
|
||||
write_your_username: "Edit your username"
|
||||
your_username: "Your username appears on every comment you post."
|
||||
change_password:
|
||||
change_password: "Change Password"
|
||||
passwords_dont_match: "Passwords don`t match"
|
||||
required_field: "This field is required"
|
||||
forgot_password: "Forgot your password?"
|
||||
save: "Save"
|
||||
cancel: "Cancel"
|
||||
edit: "Edit"
|
||||
changed_password_msg: "Changed Password - Your password has been successfully changed"
|
||||
de:
|
||||
talk-plugin-auth:
|
||||
login:
|
||||
@@ -222,6 +231,15 @@ es:
|
||||
username: Nombre
|
||||
write_your_username: "Edita tu nombre"
|
||||
your_username: "Tu nombre aparece en cada comentario que publiques."
|
||||
change_password:
|
||||
change_password: "Cambiar Contraseña"
|
||||
passwords_dont_match: "Las contraseñas no coinciden"
|
||||
required_field: "Este campo es requerido"
|
||||
forgot_password: "Olvidaste tu contraseña?"
|
||||
save: "Guardar"
|
||||
cancel: "Cancelar"
|
||||
edit: "Editar"
|
||||
changed_password_msg: "Contraseña Actualizada - Tu contraseña ha sido exitosamente actualizada"
|
||||
fr:
|
||||
talk-plugin-auth:
|
||||
login:
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
<head>
|
||||
<meta name="viewport" content="initial-scale=1, maximum-scale=1">
|
||||
<title><%= t('talk-plugin-notifications.unsubscribe_page.unsubscribe') %></title>
|
||||
<%- include(root + '/partials/head') %>
|
||||
<link rel="stylesheet" href="https://code.getmdl.io/1.2.1/material.indigo-pink.min.css">
|
||||
<link rel="stylesheet" href="<%= BASE_PATH %>public/css/admin.css">
|
||||
<%- include(root + '/partials/head') %>
|
||||
</head>
|
||||
<body class="confirm-email-page">
|
||||
<div id="root">
|
||||
|
||||
@@ -109,20 +109,11 @@ router.put(
|
||||
async (req, res, next) => {
|
||||
const { token, password } = req.body;
|
||||
|
||||
if (!password || password.length < 8) {
|
||||
return next(errors.ErrPasswordTooShort);
|
||||
}
|
||||
|
||||
try {
|
||||
let [user, redirect] = await UsersService.verifyPasswordResetToken(token);
|
||||
|
||||
// Change the users' password.
|
||||
await UsersService.changePassword(user.id, password);
|
||||
|
||||
const { redirect } = await UsersService.resetPassword(token, password);
|
||||
res.json({ redirect });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return next(errors.ErrNotAuthorized);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
+34
-9
@@ -132,7 +132,7 @@ class Users {
|
||||
locals: {
|
||||
body: message,
|
||||
},
|
||||
subject: 'Your account has been suspended',
|
||||
subject: 'Your account has been suspended', // TODO: replace with translation
|
||||
});
|
||||
}
|
||||
|
||||
@@ -490,6 +490,10 @@ class Users {
|
||||
}
|
||||
|
||||
static async changePassword(id, password) {
|
||||
if (!password || password.length < 8) {
|
||||
throw new ErrPasswordTooShort();
|
||||
}
|
||||
|
||||
const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS);
|
||||
|
||||
return User.update(
|
||||
@@ -725,18 +729,13 @@ class Users {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies a jwt and returns the associated user. Throws an error when the
|
||||
* token isn't valid.
|
||||
*
|
||||
* @param {String} token the JSON Web Token to verify
|
||||
*/
|
||||
// TODO: update doc
|
||||
static async verifyPasswordResetToken(token) {
|
||||
if (!token) {
|
||||
throw new Error('cannot verify an empty token');
|
||||
}
|
||||
|
||||
const { userId, loc, version } = await Users.verifyToken(token, {
|
||||
const { userId, loc: redirect, version } = await Users.verifyToken(token, {
|
||||
subject: PASSWORD_RESET_JWT_SUBJECT,
|
||||
});
|
||||
|
||||
@@ -746,7 +745,33 @@ class Users {
|
||||
throw new Error('password reset token has expired');
|
||||
}
|
||||
|
||||
return [user, loc];
|
||||
return { user, redirect, version };
|
||||
}
|
||||
|
||||
// TODO: update doc
|
||||
static async resetPassword(token, password) {
|
||||
const { user, redirect, version } = await this.verifyPasswordResetToken(
|
||||
token
|
||||
);
|
||||
|
||||
if (!password || password.length < 8) {
|
||||
throw new ErrPasswordTooShort();
|
||||
}
|
||||
|
||||
const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS);
|
||||
|
||||
// Update the user's password.
|
||||
await User.update(
|
||||
{ id: user.id, __v: version },
|
||||
{
|
||||
$inc: { __v: 1 },
|
||||
$set: {
|
||||
password: hashedPassword,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return { user, redirect };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+1
-1
@@ -11,8 +11,8 @@
|
||||
<link rel="stylesheet" href="https://code.getmdl.io/1.2.1/material.min.css">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
|
||||
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" type="text/css" href="<%= resolve('coral-admin/bundle.css') %>">
|
||||
<%- include partials/head %>
|
||||
<link rel="stylesheet" type="text/css" href="<%= resolve('coral-admin/bundle.css') %>">
|
||||
</head>
|
||||
<body class="admin-page">
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no">
|
||||
<%- include ../partials/head %>
|
||||
<link rel="stylesheet" type="text/css" href="<%= resolve('embed/stream/default.css') %>">
|
||||
<link rel="stylesheet" type="text/css" href="<%= resolve('embed/stream/bundle.css') %>">
|
||||
<%- include ../partials/head %>
|
||||
</head>
|
||||
<body class="embed-stream-page">
|
||||
<div id="talk-embed-stream-container"></div>
|
||||
|
||||
+1
-1
@@ -4,8 +4,8 @@
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
|
||||
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" type="text/css" href="<%= resolve('coral-login/bundle.css') %>">
|
||||
<%- include partials/head %>
|
||||
<link rel="stylesheet" type="text/css" href="<%= resolve('coral-login/bundle.css') %>">
|
||||
</head>
|
||||
<body>
|
||||
<div id="talk-login-container"></div>
|
||||
|
||||
Reference in New Issue
Block a user