You've already forked focalboard
							
							
				mirror of
				https://github.com/mattermost/focalboard.git
				synced 2025-10-31 00:17:42 +02:00 
			
		
		
		
	Change password
This commit is contained in:
		| @@ -42,6 +42,7 @@ func (a *API) RegisterRoutes(r *mux.Router) { | ||||
|  | ||||
| 	r.HandleFunc("/api/v1/users/me", a.sessionRequired(a.handleGetMe)).Methods("GET") | ||||
| 	r.HandleFunc("/api/v1/users/{userID}", a.sessionRequired(a.handleGetUser)).Methods("GET") | ||||
| 	r.HandleFunc("/api/v1/users/{userID}/changepassword", a.sessionRequired(a.handleChangePassword)).Methods("POST") | ||||
|  | ||||
| 	r.HandleFunc("/api/v1/login", a.handleLogin).Methods("POST") | ||||
| 	r.HandleFunc("/api/v1/register", a.handleRegister).Methods("POST") | ||||
|   | ||||
| @@ -10,6 +10,7 @@ import ( | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/gorilla/mux" | ||||
| 	"github.com/mattermost/mattermost-octo-tasks/server/model" | ||||
| 	"github.com/mattermost/mattermost-octo-tasks/server/services/auth" | ||||
| ) | ||||
| @@ -39,9 +40,38 @@ func (rd *RegisterData) IsValid() error { | ||||
| 	if !strings.Contains(rd.Email, "@") { | ||||
| 		return errors.New("Invalid email format") | ||||
| 	} | ||||
| 	if !strings.Contains(rd.Password, "") { | ||||
| 	if rd.Password == "" { | ||||
| 		return errors.New("Password is required") | ||||
| 	} | ||||
| 	if err := isValidPassword(rd.Password); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| type ChangePasswordData struct { | ||||
| 	OldPassword string `json:"oldPassword"` | ||||
| 	NewPassword string `json:"newPassword"` | ||||
| } | ||||
|  | ||||
| func (rd *ChangePasswordData) IsValid() error { | ||||
| 	if rd.OldPassword == "" { | ||||
| 		return errors.New("Old password is required") | ||||
| 	} | ||||
| 	if rd.NewPassword == "" { | ||||
| 		return errors.New("New password is required") | ||||
| 	} | ||||
| 	if err := isValidPassword(rd.NewPassword); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func isValidPassword(password string) error { | ||||
| 	if len(password) < 8 { | ||||
| 		return errors.New("Password must be at least 8 characters") | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| @@ -131,6 +161,35 @@ func (a *API) handleRegister(w http.ResponseWriter, r *http.Request) { | ||||
| 	jsonBytesResponse(w, http.StatusOK, nil) | ||||
| } | ||||
|  | ||||
| func (a *API) handleChangePassword(w http.ResponseWriter, r *http.Request) { | ||||
| 	vars := mux.Vars(r) | ||||
| 	userID := vars["userID"] | ||||
|  | ||||
| 	requestBody, err := ioutil.ReadAll(r.Body) | ||||
| 	if err != nil { | ||||
| 		errorResponse(w, http.StatusInternalServerError, nil, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var requestData ChangePasswordData | ||||
| 	if err := json.Unmarshal(requestBody, &requestData); err != nil { | ||||
| 		errorResponse(w, http.StatusInternalServerError, nil, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if err = requestData.IsValid(); err != nil { | ||||
| 		errorResponse(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if err = a.app().ChangePassword(userID, requestData.OldPassword, requestData.NewPassword); err != nil { | ||||
| 		errorResponse(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	jsonBytesResponse(w, http.StatusOK, nil) | ||||
| } | ||||
|  | ||||
| func (a *API) sessionRequired(handler func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) { | ||||
| 	return a.attachSession(handler, true) | ||||
| } | ||||
|   | ||||
| @@ -68,7 +68,7 @@ func (a *App) Login(username string, email string, password string, mfaToken str | ||||
| 	} | ||||
|  | ||||
| 	if !auth.ComparePassword(user.Password, password) { | ||||
| 		log.Printf("Not valid passowrd. %s (%s)\n", password, user.Password) | ||||
| 		log.Printf("Invalid password for userID: %s\n", user.ID) | ||||
| 		return "", errors.New("invalid username or password") | ||||
| 	} | ||||
|  | ||||
| @@ -132,3 +132,30 @@ func (a *App) RegisterUser(username string, email string, password string) error | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (a *App) ChangePassword(userID string, oldPassword string, newPassword string) error { | ||||
| 	var user *model.User | ||||
| 	if userID != "" { | ||||
| 		var err error | ||||
| 		user, err = a.store.GetUserById(userID) | ||||
| 		if err != nil { | ||||
| 			return errors.Wrap(err, "invalid username or password") | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if user == nil { | ||||
| 		return errors.New("invalid username or password") | ||||
| 	} | ||||
|  | ||||
| 	if !auth.ComparePassword(user.Password, oldPassword) { | ||||
| 		log.Printf("Invalid password for userID: %s\n", user.ID) | ||||
| 		return errors.New("invalid username or password") | ||||
| 	} | ||||
|  | ||||
| 	err := a.store.UpdateUserPasswordByID(userID, auth.HashPassword(newPassword)) | ||||
| 	if err != nil { | ||||
| 		return errors.Wrap(err, "unable to update password") | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|   | ||||
| @@ -93,3 +93,15 @@ func (s *SQLStore) UpdateUser(user *model.User) error { | ||||
| 	_, err = query.Exec() | ||||
| 	return err | ||||
| } | ||||
|  | ||||
| func (s *SQLStore) UpdateUserPasswordByID(userID string, password string) error { | ||||
| 	now := time.Now().Unix() | ||||
|  | ||||
| 	query := s.getQueryBuilder().Update("users"). | ||||
| 		Set("password", password). | ||||
| 		Set("update_at", now). | ||||
| 		Where(sq.Eq{"id": userID}) | ||||
|  | ||||
| 	_, err := query.Exec() | ||||
| 	return err | ||||
| } | ||||
|   | ||||
| @@ -27,6 +27,7 @@ type Store interface { | ||||
| 	GetUserByUsername(username string) (*model.User, error) | ||||
| 	CreateUser(user *model.User) error | ||||
| 	UpdateUser(user *model.User) error | ||||
| 	UpdateUserPasswordByID(userID string, password string) error | ||||
|  | ||||
| 	GetSession(token string, expireTime int64) (*model.Session, error) | ||||
| 	CreateSession(session *model.Session) error | ||||
|   | ||||
| @@ -10,6 +10,7 @@ | ||||
|   "BoardComponent.no-property": "No {property}", | ||||
|   "BoardComponent.no-property-title": "Items with an empty {property} property will go here. This column cannot be removed.", | ||||
|   "BoardComponent.show": "Show", | ||||
|   "BoardPage.syncFailed": "Board may be deleted or access revoked.", | ||||
|   "CardDetail.add-content": "Add content", | ||||
|   "CardDetail.add-icon": "Add icon", | ||||
|   "CardDetail.add-property": "+ Add a property", | ||||
| @@ -63,6 +64,7 @@ | ||||
|   "RegistrationLink.confirmRegenerateToken": "This will invalidate previously shared links. Continue?", | ||||
|   "RegistrationLink.copiedLink": "Copied!", | ||||
|   "RegistrationLink.copyLink": "Copy link", | ||||
|   "RegistrationLink.description": "Share this link for others to create accounts:", | ||||
|   "RegistrationLink.regenerateToken": "Regenerate token", | ||||
|   "RegistrationLink.tokenRegenerated": "Registration link regenerated", | ||||
|   "ShareBoard.confirmRegenerateToken": "This will invalidate previously shared links. Continue?", | ||||
| @@ -74,6 +76,7 @@ | ||||
|   "ShareBoard.unshare": "Anyone with the link can view this board", | ||||
|   "Sidebar.add-board": "+ Add Board", | ||||
|   "Sidebar.add-template": "+ New template", | ||||
|   "Sidebar.changePassword": "Change password", | ||||
|   "Sidebar.dark-theme": "Dark theme", | ||||
|   "Sidebar.default-theme": "Default theme", | ||||
|   "Sidebar.delete-board": "Delete board", | ||||
| @@ -86,6 +89,7 @@ | ||||
|   "Sidebar.import-archive": "Import archive", | ||||
|   "Sidebar.invite-users": "Invite Users", | ||||
|   "Sidebar.light-theme": "Light theme", | ||||
|   "Sidebar.logout": "Log out", | ||||
|   "Sidebar.no-views-in-board": "No pages inside", | ||||
|   "Sidebar.select-a-template": "Select a template", | ||||
|   "Sidebar.set-language": "Set language", | ||||
|   | ||||
| @@ -2,24 +2,21 @@ | ||||
| // See LICENSE.txt for license information. | ||||
| import React from 'react' | ||||
| import {IntlProvider} from 'react-intl' | ||||
|  | ||||
| import { | ||||
|     BrowserRouter as Router, | ||||
|     Switch, | ||||
|     Route, | ||||
|     Redirect, | ||||
|     Route, | ||||
|     Switch, | ||||
| } from 'react-router-dom' | ||||
|  | ||||
| import client from './octoClient' | ||||
| import {IUser, UserContext} from './user' | ||||
|  | ||||
| import {getCurrentLanguage, getMessages, storeLanguage} from './i18n' | ||||
|  | ||||
| import {FlashMessages} from './components/flashMessages' | ||||
|  | ||||
| import {getCurrentLanguage, getMessages, storeLanguage} from './i18n' | ||||
| import client from './octoClient' | ||||
| import BoardPage from './pages/boardPage' | ||||
| import ChangePasswordPage from './pages/changePasswordPage' | ||||
| import LoginPage from './pages/loginPage' | ||||
| import RegisterPage from './pages/registerPage' | ||||
| import BoardPage from './pages/boardPage' | ||||
| import {IUser, UserContext} from './user' | ||||
|  | ||||
| type State = { | ||||
|     language: string, | ||||
| @@ -65,6 +62,9 @@ export default class App extends React.PureComponent<unknown, State> { | ||||
|                                     <Route path='/register'> | ||||
|                                         <RegisterPage/> | ||||
|                                     </Route> | ||||
|                                     <Route path='/change_password'> | ||||
|                                         <ChangePasswordPage/> | ||||
|                                     </Route> | ||||
|                                     <Route path='/shared'> | ||||
|                                         <BoardPage | ||||
|                                             readonly={true} | ||||
|   | ||||
| @@ -103,6 +103,13 @@ class Sidebar extends React.Component<Props, State> { | ||||
|                                                 window.location.href = '/login' | ||||
|                                             }} | ||||
|                                         /> | ||||
|                                         <Menu.Text | ||||
|                                             id='changePassword' | ||||
|                                             name={intl.formatMessage({id: 'Sidebar.changePassword', defaultMessage: 'Change password'})} | ||||
|                                             onClick={async () => { | ||||
|                                                 window.location.href = '/change_password' | ||||
|                                             }} | ||||
|                                         /> | ||||
|                                     </Menu> | ||||
|                                 </MenuWrapper> | ||||
|  | ||||
|   | ||||
| @@ -70,6 +70,18 @@ class OctoClient { | ||||
|         return {code: response.status, json} | ||||
|     } | ||||
|  | ||||
|     async changePassword(userId: string, oldPassword: string, newPassword: string): Promise<{code: number, json: any}> { | ||||
|         const path = `/api/v1/users/${encodeURIComponent(userId)}/changepassword` | ||||
|         const body = JSON.stringify({oldPassword, newPassword}) | ||||
|         const response = await fetch(this.serverUrl + path, { | ||||
|             method: 'POST', | ||||
|             headers: this.headers(), | ||||
|             body, | ||||
|         }) | ||||
|         const json = (await this.getJson(response)) | ||||
|         return {code: response.status, json} | ||||
|     } | ||||
|  | ||||
|     private headers() { | ||||
|         return { | ||||
|             Accept: 'application/json', | ||||
|   | ||||
							
								
								
									
										64
									
								
								webapp/src/pages/changePasswordPage.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								webapp/src/pages/changePasswordPage.scss
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,64 @@ | ||||
| .ChangePasswordPage { | ||||
|     border: 1px solid #cccccc; | ||||
|     border-radius: 15px; | ||||
|     width: 450px; | ||||
|     height: 400px; | ||||
|     margin: 150px auto; | ||||
|     padding: 40px; | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     justify-content: flex-start; | ||||
|     flex-direction: column; | ||||
|     box-shadow: rgba(var(--main-fg), 0.1) 0px 0px 0px 1px, rgba(var(--main-fg), 0.3) 0px 4px 8px; | ||||
|  | ||||
|     @media screen and (max-width: 430px) { | ||||
|         position: fixed; | ||||
|         top: 0; | ||||
|         left: 0; | ||||
|         right: 0; | ||||
|         bottom: 0; | ||||
|         width: 100%; | ||||
|         height: 100%; | ||||
|         margin: auto; | ||||
|         padding-top: 10px; | ||||
|     } | ||||
|  | ||||
|     .title { | ||||
|         font-size: 16px; | ||||
|         font-weight: 500; | ||||
|     } | ||||
|  | ||||
|     .oldPassword, .newPassword { | ||||
|         margin-bottom: 10px; | ||||
|  | ||||
|         label { | ||||
|             display: inline-block; | ||||
|             width: 140px; | ||||
|         } | ||||
|  | ||||
|         input { | ||||
|             display: inline-block; | ||||
|             width: 250px; | ||||
|             border: 1px solid #cccccc; | ||||
|             border-radius: 4px; | ||||
|             padding: 7px; | ||||
|             min-height: 44px; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     > .Button { | ||||
|         margin-top: 10px; | ||||
|         margin-bottom: 20px; | ||||
|         min-height: 38px; | ||||
|         min-width: 250px; | ||||
|     } | ||||
|  | ||||
|     .error { | ||||
|         color: #900000; | ||||
|     } | ||||
|  | ||||
|     .succeeded { | ||||
|         background-color: #ccffcc; | ||||
|         padding: 5px; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										104
									
								
								webapp/src/pages/changePasswordPage.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								webapp/src/pages/changePasswordPage.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,104 @@ | ||||
| // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. | ||||
| // See LICENSE.txt for license information. | ||||
| import React from 'react' | ||||
|  | ||||
| import { | ||||
|     withRouter, | ||||
|     RouteComponentProps, | ||||
|     Link, | ||||
| } from 'react-router-dom' | ||||
|  | ||||
| import Button from '../widgets/buttons/button' | ||||
| import client from '../octoClient' | ||||
| import './changePasswordPage.scss' | ||||
| import {UserContext} from '../user' | ||||
|  | ||||
| type Props = RouteComponentProps | ||||
|  | ||||
| type State = { | ||||
|     oldPassword: string | ||||
|     newPassword: string | ||||
|     errorMessage?: string | ||||
|     succeeded: boolean | ||||
| } | ||||
|  | ||||
| class ChangePasswordPage extends React.PureComponent<Props, State> { | ||||
|     state: State = { | ||||
|         oldPassword: '', | ||||
|         newPassword: '', | ||||
|         succeeded: false, | ||||
|     } | ||||
|  | ||||
|     private handleSubmit = async (userId: string): Promise<void> => { | ||||
|         const response = await client.changePassword(userId, this.state.oldPassword, this.state.newPassword) | ||||
|         if (response.code === 200) { | ||||
|             this.setState({succeeded: true}) | ||||
|         } else { | ||||
|             this.setState({errorMessage: `Change password failed: ${response.json?.error}`}) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private closeClicked = () => { | ||||
|         this.props.history.push('/') | ||||
|     } | ||||
|  | ||||
|     render(): React.ReactNode { | ||||
|         return ( | ||||
|             <div className='ChangePasswordPage'> | ||||
|                 <div className='title'>{'Change Password'}</div> | ||||
|  | ||||
|                 <UserContext.Consumer> | ||||
|                     {(user) => { | ||||
|                         if (user) { | ||||
|                             return (<> | ||||
|                                 <div className='oldPassword'> | ||||
|                                     <input | ||||
|                                         id='login-oldpassword' | ||||
|                                         type='password' | ||||
|                                         placeholder={'Enter current password'} | ||||
|                                         value={this.state.oldPassword} | ||||
|                                         onChange={(e) => this.setState({oldPassword: e.target.value, errorMessage: undefined})} | ||||
|                                     /> | ||||
|                                 </div> | ||||
|                                 <div className='newPassword'> | ||||
|                                     <input | ||||
|                                         id='login-newpassword' | ||||
|                                         type='password' | ||||
|                                         placeholder={'Enter new password'} | ||||
|                                         value={this.state.newPassword} | ||||
|                                         onChange={(e) => this.setState({newPassword: e.target.value, errorMessage: undefined})} | ||||
|                                     /> | ||||
|                                 </div> | ||||
|                                 <Button | ||||
|                                     filled={true} | ||||
|                                     onClick={() => this.handleSubmit(user.id)} | ||||
|                                 > | ||||
|                                     {'Change password'} | ||||
|                                 </Button> | ||||
|                                 {this.state.errorMessage && | ||||
|                                     <div className='error'> | ||||
|                                         {this.state.errorMessage} | ||||
|                                     </div> | ||||
|                                 } | ||||
|                                 {this.state.succeeded && | ||||
|                                     <Link | ||||
|                                         className='succeeded' | ||||
|                                         to='/' | ||||
|                                     >{'Password changed, click to continue.'}</Link> | ||||
|                                 } | ||||
|                                 {!this.state.succeeded && | ||||
|                                     <Link to='/'>{'Cancel'}</Link> | ||||
|                                 } | ||||
|                             </>) | ||||
|                         } | ||||
|                         return ( | ||||
|                             <Link to='/login'>{'Log in first'}</Link> | ||||
|                         ) | ||||
|                     }} | ||||
|                 </UserContext.Consumer> | ||||
|             </div> | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  | ||||
| export default withRouter(ChangePasswordPage) | ||||
| @@ -44,7 +44,7 @@ class RegisterPage extends React.PureComponent<Props, State> { | ||||
|         } else if (response.code === 401) { | ||||
|             this.setState({errorMessage: 'Invalid registration link, please contact your administrator'}) | ||||
|         } else { | ||||
|             this.setState({errorMessage: response.json.error}) | ||||
|             this.setState({errorMessage: response.json?.error}) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user