Skip to content

Commit

Permalink
Add password change modal (#1001)
Browse files Browse the repository at this point in the history
  • Loading branch information
mensinda authored Mar 1, 2024
1 parent 6aaa31a commit 5bde739
Show file tree
Hide file tree
Showing 9 changed files with 180 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.box.l10n.mojito.rest.security;

public record PasswordChangeRequest(String currentPassword, String newPassword) {}
Original file line number Diff line number Diff line change
Expand Up @@ -153,4 +153,11 @@ public void updateUserByUserId(@PathVariable Long userId, @RequestBody User user
user.getCommonName(),
false);
}

@RequestMapping(value = "/api/users/pw", method = RequestMethod.POST)
public ResponseEntity<User> changePassword(@RequestBody PasswordChangeRequest requestDTO) {
User user = userService.updatePassword(requestDTO.currentPassword(), requestDTO.newPassword());
logger.info("Updated password for user [{}]", user.getId());
return new ResponseEntity<>(user, HttpStatus.ACCEPTED);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ protected void configure(HttpSecurity http) throws Exception {
antMatchers("/actuator/shutdown", "/actuator/loggers/**", "/api/rotation")
.hasIpAddress("127.0.0.1")
. // Everyone can access the session endpoint
antMatchers("/api/users/session")
antMatchers("/api/users/session", "/api/users/pw")
.authenticated()
. // user management is only allowed for ADMINs and PMs
antMatchers("/api/users/**")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.box.l10n.mojito.security.Role;
import com.google.common.base.Preconditions;
import com.google.common.collect.Sets;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import org.apache.commons.lang3.RandomStringUtils;
Expand Down Expand Up @@ -178,6 +179,24 @@ private User saveAuthorities(User user, Role role) {
return user;
}

@Transactional
public User updatePassword(String currentPassword, String newPassword) {
Objects.requireNonNull(currentPassword);
Objects.requireNonNull(newPassword);

User user = auditorAwareImpl.getCurrentAuditor().orElseThrow();

BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
if (!bCryptPasswordEncoder.matches(currentPassword, user.getPassword())) {
throw new AccessDeniedException("Invalid current password");
}

user.setPassword(bCryptPasswordEncoder.encode(newPassword));
userRepository.save(user);

return user;
}

/**
* Create a {@link com.box.l10n.mojito.entity.security.user.User}.
*
Expand Down
6 changes: 6 additions & 0 deletions webapp/src/main/resources/properties/de.properties
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ header.screenshots=Screenshots
# Label displayed in the header menu to open the user management page
header.user-management=Benutzerverwaltung

# Label displayed in the header menu to open the change password modal
header.change-pw=Passwort Ändern

# Column header of the branch results, used to display the name of the branch
branches.header.branch=Branch

Expand Down Expand Up @@ -746,7 +749,10 @@ userEditModal.form.label.surname=Nachname
userEditModal.form.placeholder.surname=Nachname eingeben
userEditModal.form.label.commonName=Name (kann statt dem Vor- und Nachnamen verwendet werden)
userEditModal.form.placeholder.commonName=Name eingeben
userEditModal.form.label.currentPassword=Aktuelles Password
userEditModal.form.label.newPassword=Neues Password
userEditModal.form.label.password=Passwort
userEditModal.form.placeholder.currentPassword=Bitte geben Sie ihr aktuelles Passwort ein
userEditModal.form.placeholder.password=Passwort eingeben
userEditModal.form.placeholder.passwordValidation=Passwort nochmal eingeben
userEditModal.form.label.authority=Rolle
Expand Down
6 changes: 6 additions & 0 deletions webapp/src/main/resources/properties/en.properties
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ header.screenshots=Screenshots
# Label displayed in the header menu to open the user management page
header.user-management=User Management

# Label displayed in the header menu to open the change password modal
header.change-pw=Change Password

# Column header of the branch results, used to display the name of the branch
branches.header.branch=Branch

Expand Down Expand Up @@ -817,7 +820,10 @@ userEditModal.form.label.surname=Surname
userEditModal.form.placeholder.surname=Enter the surname
userEditModal.form.label.commonName=Common Name (can be used instead of given name + surname)
userEditModal.form.placeholder.commonName=Enter the common name
userEditModal.form.label.currentPassword=Current Password
userEditModal.form.label.newPassword=New Password
userEditModal.form.label.password=Password
userEditModal.form.placeholder.currentPassword=Enter your current password
userEditModal.form.placeholder.password=Enter the password
userEditModal.form.placeholder.passwordValidation=Re-enter the password
userEditModal.form.label.authority=Role
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import React from "react";
import {Button, FormControl, Modal, FormGroup, ControlLabel, Alert, Collapse} from "react-bootstrap";
import {FormattedMessage, injectIntl} from "react-intl";
import UserStatics from "../../utils/UserStatics";
import UserClient from "../../sdk/UserClient";

let createClass = require('create-react-class');

let ChangePasswordModal = createClass({

getInitialState() {
return {
currPasswordValue: '',
newPasswordValue: '',
newPasswordValidationValue: '',
showAlert: false,
};
},

isValidPassword() {
let passwordState = this.state.newPasswordValue;
let passwordValidationState = this.state.newPasswordValidationValue;

let isPasswordEmpty = passwordState.length <= 0 || passwordValidationState.length <= 0;
let isPasswordLengthExceeded = passwordState.length > UserStatics.modalFormMaxLength() || passwordValidationState.length > UserStatics.modalFormMaxLength();

return !isPasswordEmpty && !isPasswordLengthExceeded && passwordState === passwordValidationState;
},

onSaveClicked() {
UserClient
.updatePassword(this.state.currPasswordValue, this.state.newPasswordValue)
.then(() => {
this.doClose();
})
.catch(() => {
this.setState({showAlert: true});
});
},

getAlert() {
return (
<Collapse in={this.state.showAlert}>
<div>
<Alert bsStyle="danger">
<p className="text-center text-muted"><FormattedMessage id="userEditModal.alertMessage"/></p>
</Alert>
</div>
</Collapse>
);
},

doClose() {
this.setState(this.getInitialState());
this.props.onClose();
},

render() {
return (
<Modal show={this.props.show} onHide={this.doClose}>
<Modal.Header>
<Modal.Title className="text-center">
<span className="mutedBold"><FormattedMessage id="header.change-pw"/></span></Modal.Title>
</Modal.Header>
<Modal.Body>
{this.getAlert()}
<FormGroup validationState={this.state.currPasswordValue ? "success" : "error"}>
<ControlLabel className="mutedBold pbs"><FormattedMessage id="userEditModal.form.label.currentPassword"/></ControlLabel>
<FormControl id="current password input"
onChange={(e) => this.setState({currPasswordValue: e.target.value})}
type="password" value={this.state.currPasswordValue}
placeholder={this.props.intl.formatMessage({ id: "userEditModal.form.placeholder.currentPassword" })}/>
</FormGroup>

<hr />

<FormGroup validationState={this.isValidPassword() ? "success" : "error"}>
<ControlLabel className="mutedBold pbs"><FormattedMessage id="userEditModal.form.label.newPassword"/></ControlLabel>
<FormControl id="password input"
onChange={(e) => this.setState({newPasswordValue: e.target.value})}
type="password" value={this.state.newPasswordValue}
placeholder={this.props.intl.formatMessage({ id: "userEditModal.form.placeholder.password" })}/>
<FormControl id="password validation input"
className="mtm"
onChange={(e) => this.setState({newPasswordValidationValue: e.target.value})}
type="password" value={this.state.newPasswordValidationValue}
placeholder={this.props.intl.formatMessage({ id: "userEditModal.form.placeholder.passwordValidation" })}/>
</FormGroup>
</Modal.Body>
<Modal.Footer>
<div className="text-center">
<Button bsStyle="primary" onClick={this.onSaveClicked} disabled={!this.isValidPassword() || !this.state.currPasswordValue}>
<FormattedMessage id="label.save"/>
</Button>
<Button onClick={this.doClose}>
<FormattedMessage id="label.cancel"/>
</Button>
</div>
</Modal.Footer>
</Modal>
);
}
});

export default injectIntl(ChangePasswordModal);
30 changes: 26 additions & 4 deletions webapp/src/main/resources/public/js/components/header/Header.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import LocaleSelectorModal from "./LocaleSelectorModal";
import Locales from "../../utils/Locales";
import UrlHelper from "../../utils/UrlHelper";

import ChangePasswordModal from "./ChangePasswordModal";

import RepositoryActions from "../../actions/RepositoryActions";
import WorkbenchActions from "../../actions/workbench/WorkbenchActions";
import ScreenshotsPageActions from "../../actions/screenshots/ScreenshotsPageActions";
Expand All @@ -19,7 +21,8 @@ import {withAppConfig} from "../../utils/AppConfig";

class Header extends React.Component {
state = {
showLocaleSelectorModal: false
showLocaleSelectorModal: false,
showChangePasswordModal: false,
};

logoutSelected = () => {
Expand All @@ -39,6 +42,18 @@ class Header extends React.Component {
});
};

openChangePasswordModal = () => {
this.setState({
showChangePasswordModal: true
});
};

closeChangePasswordModal = () => {
this.setState({
showChangePasswordModal: false
});
};

/**
* Update the Workbench search params to load the default view
*
Expand All @@ -54,9 +69,9 @@ class Header extends React.Component {
const user = this.props.appConfig.user;
let usernameDisplay = user.username;

if (user.givenName != null) {
if (user.givenName) {
usernameDisplay = user.givenName;
} else if (user.commonName != null) {
} else if (user.commonName) {
usernameDisplay = user.commonName;
}

Expand Down Expand Up @@ -113,16 +128,23 @@ class Header extends React.Component {
</LinkContainer>

<MenuItem divider/>

<MenuItem onSelect={this.openChangePasswordModal}>
<Glyphicon glyph="pencil"/> <FormattedMessage id="header.change-pw"/>
</MenuItem>

<MenuItem onSelect={this.logoutSelected}>
<form action={UrlHelper.getUrlWithContextPath("/logout")} method="post" ref="logoutForm">
<FormControl type="hidden" name="_csrf" value={this.props.appConfig.csrfToken}/>
<FormattedMessage id="header.loggedInUser.logout"/>
<Glyphicon glyph="log-out"/> <FormattedMessage id="header.loggedInUser.logout"/>
</form>
</MenuItem>
</NavDropdown>
</Nav>
<LocaleSelectorModal key="headerLocaleSelectorModal" show={this.state.showLocaleSelectorModal}
onClose={this.closeLocaleSelectorModal}/>
<ChangePasswordModal key="headerChangePasswordModal" show={this.state.showChangePasswordModal}
onClose={this.closeChangePasswordModal}/>
</Navbar>
);
}
Expand Down
7 changes: 7 additions & 0 deletions webapp/src/main/resources/public/js/sdk/UserClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ class UserClient extends BaseClient {
return this.patch(this.getUrl(parmas.id), parmas.user);
}

updatePassword(currentPassword, newPassword) {
return this.post(this.getUrl() + "/pw", {
currentPassword: currentPassword,
newPassword: newPassword,
});
}

getEntityName() {
return 'users';
}
Expand Down

0 comments on commit 5bde739

Please sign in to comment.