diff --git a/frontend/src/components/dialog/remove-webdav-password.js b/frontend/src/components/dialog/remove-webdav-password.js new file mode 100644 index 0000000000..a7ac7e6314 --- /dev/null +++ b/frontend/src/components/dialog/remove-webdav-password.js @@ -0,0 +1,53 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { Modal, ModalHeader, ModalBody, ModalFooter, Alert, Button, Input, InputGroup, InputGroupAddon } from 'reactstrap'; +import { gettext } from '../../utils/constants'; +import { Utils } from '../../utils/utils'; + +const propTypes = { + removePassword: PropTypes.func.isRequired, + toggle: PropTypes.func.isRequired +}; + +class RemoveWebdavPassword extends Component { + + constructor(props) { + super(props); + this.state = { + btnDisabled: false, + errMsg: '' + }; + } + + submit = () => { + + this.setState({ + btnDisabled: true + }); + + this.props.removePassword(); + } + + render() { + const { toggle } = this.props; + let dialogMsg = gettext('Are you sure you want to remove {placeholder} ?').replace('{placeholder}', 'WebDAV password'); + + return ( + + {gettext('Remove WebDAV Password')} + +

{dialogMsg}

+
+ {this.state.errMsg && {gettext(this.state.errMsg)}} + + + + +
+ ); + } +} + +RemoveWebdavPassword.propTypes = propTypes; + +export default RemoveWebdavPassword; diff --git a/frontend/src/components/dialog/update-webdav-password.js b/frontend/src/components/dialog/reset-webdav-password.js similarity index 89% rename from frontend/src/components/dialog/update-webdav-password.js rename to frontend/src/components/dialog/reset-webdav-password.js index 2d5fc24983..b35318601a 100644 --- a/frontend/src/components/dialog/update-webdav-password.js +++ b/frontend/src/components/dialog/reset-webdav-password.js @@ -5,19 +5,18 @@ import { gettext } from '../../utils/constants'; import { Utils } from '../../utils/utils'; const propTypes = { - password: PropTypes.string.isRequired, - updatePassword: PropTypes.func.isRequired, + resetPassword: PropTypes.func.isRequired, toggle: PropTypes.func.isRequired }; const { webdavSecretMinLength, webdavSecretStrengthLevel } = window.app.pageOptions; -class UpdateWebdavPassword extends Component { +class ResetWebdavPassword extends Component { constructor(props) { super(props); this.state = { - password: this.props.password, + password: '', isPasswordVisible: false, btnDisabled: false, errMsg: '' @@ -44,7 +43,7 @@ class UpdateWebdavPassword extends Component { btnDisabled: true }); - this.props.updatePassword(this.state.password.trim()); + this.props.resetPassword(this.state.password.trim()); } handleInputChange = (e) => { @@ -71,7 +70,7 @@ class UpdateWebdavPassword extends Component { return ( - {gettext('WebDav Password')} + {gettext('Reset WebDAV Password')} @@ -92,6 +91,6 @@ class UpdateWebdavPassword extends Component { } } -UpdateWebdavPassword.propTypes = propTypes; +ResetWebdavPassword.propTypes = propTypes; -export default UpdateWebdavPassword; +export default ResetWebdavPassword; diff --git a/frontend/src/components/dialog/set-webdav-password.js b/frontend/src/components/dialog/set-webdav-password.js new file mode 100644 index 0000000000..3934b0ee40 --- /dev/null +++ b/frontend/src/components/dialog/set-webdav-password.js @@ -0,0 +1,96 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { Modal, ModalHeader, ModalBody, ModalFooter, Alert, Button, Input, InputGroup, InputGroupAddon } from 'reactstrap'; +import { gettext } from '../../utils/constants'; +import { Utils } from '../../utils/utils'; + +const propTypes = { + setPassword: PropTypes.func.isRequired, + toggle: PropTypes.func.isRequired +}; + +const { webdavSecretMinLength, webdavSecretStrengthLevel } = window.app.pageOptions; + +class SetWebdavPassword extends Component { + + constructor(props) { + super(props); + this.state = { + password: '', + isPasswordVisible: false, + btnDisabled: false, + errMsg: '' + }; + } + + submit = () => { + + if (this.state.password.length === 0) { + this.setState({errMsg: gettext('Please enter a password.')}); + return false; + } + if (this.state.password.length < webdavSecretMinLength) { + this.setState({errMsg: gettext('The password is too short.')}); + return false; + } + + if (Utils.getStrengthLevel(this.state.password) < webdavSecretStrengthLevel) { + this.setState({errMsg: gettext('The password is too weak. It should include at least {passwordStrengthLevel} of the following: number, upper letter, lower letter and other symbols.').replace('{passwordStrengthLevel}', webdavSecretStrengthLevel)}); + return false; + } + + this.setState({ + btnDisabled: true + }); + + this.props.setPassword(this.state.password.trim()); + } + + handleInputChange = (e) => { + this.setState({password: e.target.value}); + } + + togglePasswordVisible = () => { + this.setState({ + isPasswordVisible: !this.state.isPasswordVisible + }); + } + + generatePassword = () => { + let randomPassword = Utils.generatePassword(webdavSecretMinLength); + this.setState({ + password: randomPassword, + isPasswordVisible: true + }); + } + + render() { + const { toggle } = this.props; + const passwordTip = gettext('(at least {passwordMinLength} characters and includes {passwordStrengthLevel} of the following: number, upper letter, lower letter and other symbols)').replace('{passwordMinLength}', webdavSecretMinLength).replace('{passwordStrengthLevel}', webdavSecretStrengthLevel); + + return ( + + {gettext('Set WebDAV Password')} + + + + + + + + +

{passwordTip}

+ {this.state.errMsg && {gettext(this.state.errMsg)}} +
+ + + + +
+ ); + } +} + +SetWebdavPassword.propTypes = propTypes; + +export default SetWebdavPassword; diff --git a/frontend/src/components/user-settings/webdav-password.js b/frontend/src/components/user-settings/webdav-password.js index a8c2983e73..cf55f9f8ed 100644 --- a/frontend/src/components/user-settings/webdav-password.js +++ b/frontend/src/components/user-settings/webdav-password.js @@ -4,44 +4,78 @@ import { gettext } from '../../utils/constants'; import { seafileAPI } from '../../utils/seafile-api'; import { Utils } from '../../utils/utils'; import toaster from '../toast'; -import UpdateWebdavPassword from '../dialog/update-webdav-password'; +import SetWebdavPassword from '../dialog/set-webdav-password'; +import ResetWebdavPassword from '../dialog/reset-webdav-password'; +import RemoveWebdavPassword from '../dialog/remove-webdav-password'; -const { webdavPasswd } = window.app.pageOptions; +const { username, webdavUrl, webdavPasswordSetted } = window.app.pageOptions; class WebdavPassword extends React.Component { constructor(props) { super(props); this.state = { - password: webdavPasswd, - isPasswordVisible: false, - isDialogOpen: false + isWebdavPasswordSetted: webdavPasswordSetted, + isSetPasserdDialogOpen: false, + isResetPasserdDialogOpen: false, + isRemovePasserdDialogOpen: false, }; } - togglePasswordVisible = () => { + toggleSetPasswordDialog = () => { this.setState({ - isPasswordVisible: !this.state.isPasswordVisible + isSetPasserdDialogOpen: !this.state.isSetPasserdDialogOpen, }); } - updatePassword = (password) => { + setPassword = (password) => { seafileAPI.updateWebdavSecret(password).then((res) => { - this.toggleDialog(); + this.toggleSetPasswordDialog(); this.setState({ - password: password + isWebdavPasswordSetted: !this.state.isWebdavPasswordSetted, }); toaster.success(gettext('Success')); }).catch((error) => { let errorMsg = Utils.getErrorMsg(error); - this.toggleDialog(); + this.toggleSetPasswordDialog(); toaster.danger(errorMsg); }); } - toggleDialog = () => { + toggleResetPasswordDialog = () => { this.setState({ - isDialogOpen: !this.state.isDialogOpen + isResetPasswordDialogOpen: !this.state.isResetPasswordDialogOpen, + }); + } + + resetPassword = (password) => { + seafileAPI.updateWebdavSecret(password).then((res) => { + this.toggleResetPasswordDialog(); + toaster.success(gettext('Success')); + }).catch((error) => { + let errorMsg = Utils.getErrorMsg(error); + this.toggleResetPasswordDialog(); + toaster.danger(errorMsg); + }); + } + + toggleRemovePasswordDialog = () => { + this.setState({ + isRemovePasswordDialogOpen: !this.state.isRemovePasswordDialogOpen, + }); + } + + removePassword = () => { + seafileAPI.updateWebdavSecret().then((res) => { + this.toggleRemovePasswordDialog(); + this.setState({ + isWebdavPasswordSetted: !this.state.isWebdavPasswordSetted, + }); + toaster.success(gettext('Success')); + }).catch((error) => { + let errorMsg = Utils.getErrorMsg(error); + this.toggleRemovePasswordDialog(); + toaster.danger(errorMsg); }); } @@ -52,30 +86,47 @@ class WebdavPassword extends React.Component { } render() { - const { password, isPasswordVisible } = this.state; + const { isWebdavPasswordSetted } = this.state; return (
-

{gettext('WebDav Password')}

- {password ? ( - -
- - - -
- -
- ) : ( - - )} +

{gettext('WebDAV Password')}

+

{gettext('WebDAV URL:')} {webdavUrl}

+

{gettext('WebDAV username:')} {username}

+ {!isWebdavPasswordSetted ? + +

{gettext('WebDAV password:')} {gettext('not set')}

+ +
+ : + +

{gettext('WebDAV password:')} ***

+ + +
+ }
- {this.state.isDialogOpen && ( + {this.state.isSetPasserdDialogOpen && ( - + + )} + {this.state.isResetPasswordDialogOpen && ( + + + + )} + {this.state.isRemovePasswordDialogOpen && ( + + )} diff --git a/seahub/api2/endpoints/webdav_secret.py b/seahub/api2/endpoints/webdav_secret.py index 5d49e5189f..51cc36fc09 100644 --- a/seahub/api2/endpoints/webdav_secret.py +++ b/seahub/api2/endpoints/webdav_secret.py @@ -13,8 +13,8 @@ from seahub.api2.authentication import TokenAuthentication from seahub.api2.throttling import UserRateThrottle from seahub.api2.utils import api_error from seahub.options.models import UserOptions -from seahub.utils.hasher import AESPasswordHasher -from seahub.utils import get_password_strength_level +from seahub.utils import get_password_strength_level, \ + is_valid_password, hash_password # Get an instance of a logger logger = logging.getLogger(__name__) @@ -43,12 +43,15 @@ class WebdavSecretView(APIView): return api_error(status.HTTP_403_FORBIDDEN, 'Feature is not enabled.') - aes = AESPasswordHasher() - username = request.user.username secret = request.data.get("secret", None) if secret: + + if not is_valid_password(secret): + error_msg = _('Password can only contain number, upper letter, lower letter and other symbols.') + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + if len(secret) >= 30: error_msg = _('Length of WebDav password should be less than 30.') return api_error(status.HTTP_400_BAD_REQUEST, error_msg) @@ -61,9 +64,9 @@ class WebdavSecretView(APIView): error_msg = _('Password is too weak.') return api_error(status.HTTP_400_BAD_REQUEST, error_msg) - encoded = aes.encode(secret) - UserOptions.objects.set_webdav_secret(username, encoded) + hashed_password = hash_password(secret) + UserOptions.objects.set_webdav_secret(username, hashed_password) else: UserOptions.objects.unset_webdav_secret(username) - return self.get(request, format) + return Response({'success': True}) diff --git a/seahub/options/models.py b/seahub/options/models.py index 19ff6d1fa9..3166b55279 100644 --- a/seahub/options/models.py +++ b/seahub/options/models.py @@ -267,7 +267,7 @@ class UserOptionsManager(models.Manager): from seahub.utils.hasher import AESPasswordHasher secret = UserOptions.objects.get_webdav_secret(username) - if secret: + if secret and secret.startswith(AESPasswordHasher.algorithm): aes = AESPasswordHasher() decoded = aes.decode(secret) else: diff --git a/seahub/profile/templates/profile/set_profile_react.html b/seahub/profile/templates/profile/set_profile_react.html index 09139697c2..5d99eb384d 100644 --- a/seahub/profile/templates/profile/set_profile_react.html +++ b/seahub/profile/templates/profile/set_profile_react.html @@ -12,6 +12,7 @@