mirror of
https://github.com/mattermost/focalboard.git
synced 2025-01-11 18:13:52 +02:00
Change password
This commit is contained in:
parent
7eed80d3a6
commit
1286349a22
@ -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})
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user