web UI/REST API: add password reset

In order to reset the password from the admin/client user interface,
an SMTP configuration must be added and the user/admin must have an email
address.
You can prohibit the reset functionality on a per-user basis by using a
specific restriction.

Fixes #597
This commit is contained in:
Nicola Murino
2021-11-13 13:25:43 +01:00
parent b331dc5686
commit 78233ff9a3
25 changed files with 1787 additions and 60 deletions

View File

@@ -3,6 +3,7 @@ package httpd
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"html/template"
"io"
@@ -22,6 +23,7 @@ import (
"github.com/drakkan/sftpgo/v2/dataprovider"
"github.com/drakkan/sftpgo/v2/mfa"
"github.com/drakkan/sftpgo/v2/sdk"
"github.com/drakkan/sftpgo/v2/smtp"
"github.com/drakkan/sftpgo/v2/util"
"github.com/drakkan/sftpgo/v2/version"
"github.com/drakkan/sftpgo/v2/vfs"
@@ -48,6 +50,8 @@ const (
pageClientChangePwdTitle = "Change password"
pageClient2FATitle = "Two-factor auth"
pageClientEditFileTitle = "Edit file"
pageClientForgotPwdTitle = "SFTPGo WebClient - Forgot password"
pageClientResetPwdTitle = "SFTPGo WebClient - Reset password"
)
// condResult is the result of an HTTP request precondition check.
@@ -219,6 +223,12 @@ func loadClientTemplates(templatesPath string) {
filepath.Join(templatesPath, templateClientDir, templateClientBaseLogin),
filepath.Join(templatesPath, templateClientDir, templateClientTwoFactorRecovery),
}
forgotPwdPaths := []string{
filepath.Join(templatesPath, templateCommonDir, templateForgotPassword),
}
resetPwdPaths := []string{
filepath.Join(templatesPath, templateCommonDir, templateResetPassword),
}
filesTmpl := util.LoadTemplate(nil, filesPaths...)
profileTmpl := util.LoadTemplate(nil, profilePaths...)
@@ -231,6 +241,8 @@ func loadClientTemplates(templatesPath string) {
editFileTmpl := util.LoadTemplate(nil, editFilePath...)
sharesTmpl := util.LoadTemplate(nil, sharesPaths...)
shareTmpl := util.LoadTemplate(nil, sharePaths...)
forgotPwdTmpl := util.LoadTemplate(nil, forgotPwdPaths...)
resetPwdTmpl := util.LoadTemplate(nil, resetPwdPaths...)
clientTemplates[templateClientFiles] = filesTmpl
clientTemplates[templateClientProfile] = profileTmpl
@@ -243,6 +255,8 @@ func loadClientTemplates(templatesPath string) {
clientTemplates[templateClientEditFile] = editFileTmpl
clientTemplates[templateClientShares] = sharesTmpl
clientTemplates[templateClientShare] = shareTmpl
clientTemplates[templateForgotPassword] = forgotPwdTmpl
clientTemplates[templateResetPassword] = resetPwdTmpl
}
func getBaseClientPageData(title, currentURL string, r *http.Request) baseClientPage {
@@ -273,6 +287,28 @@ func getBaseClientPageData(title, currentURL string, r *http.Request) baseClient
}
}
func renderClientForgotPwdPage(w http.ResponseWriter, error string) {
data := forgotPwdPage{
CurrentURL: webClientForgotPwdPath,
Error: error,
CSRFToken: createCSRFToken(),
StaticURL: webStaticFilesPath,
Title: pageClientForgotPwdTitle,
}
renderClientTemplate(w, templateForgotPassword, data)
}
func renderClientResetPwdPage(w http.ResponseWriter, error string) {
data := resetPwdPage{
CurrentURL: webClientResetPwdPath,
Error: error,
CSRFToken: createCSRFToken(),
StaticURL: webStaticFilesPath,
Title: pageClientResetPwdTitle,
}
renderClientTemplate(w, templateResetPassword, data)
}
func renderClientTemplate(w http.ResponseWriter, tmplName string, data interface{}) {
err := clientTemplates[tmplName].ExecuteTemplate(w, tmplName, data)
if err != nil {
@@ -957,3 +993,45 @@ func getShareFromPostFields(r *http.Request) (*dataprovider.Share, error) {
share.ExpiresAt = expirationDateMillis
return share, nil
}
func handleWebClientForgotPwd(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
if !smtp.IsEnabled() {
renderClientNotFoundPage(w, r, errors.New("this page does not exist"))
return
}
renderClientForgotPwdPage(w, "")
}
func handleWebClientForgotPwdPost(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
err := r.ParseForm()
if err != nil {
renderClientForgotPwdPage(w, err.Error())
return
}
if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
renderClientForbiddenPage(w, r, err.Error())
return
}
username := r.Form.Get("username")
err = handleForgotPassword(r, username, false)
if err != nil {
if e, ok := err.(*util.ValidationError); ok {
renderClientForgotPwdPage(w, e.GetErrorString())
return
}
renderClientForgotPwdPage(w, err.Error())
return
}
http.Redirect(w, r, webClientResetPwdPath, http.StatusFound)
}
func handleWebClientPasswordReset(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
if !smtp.IsEnabled() {
renderClientNotFoundPage(w, r, errors.New("this page does not exist"))
return
}
renderClientResetPwdPage(w, "")
}