1
0
mirror of https://github.com/mattermost/focalboard.git synced 2025-01-11 18:13:52 +02:00

Change password

This commit is contained in:
Chen-I Lim 2021-01-21 10:16:40 -08:00
parent 7eed80d3a6
commit 1286349a22
12 changed files with 304 additions and 13 deletions

View File

@ -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")

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -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",

View File

@ -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}

View File

@ -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>

View File

@ -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',

View 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;
}
}

View 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)

View File

@ -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})
}
}