소스 검색

WebAdmin: use the new theme for the login and setup page

Signed-off-by: Nicola Murino <[email protected]>
Nicola Murino 1 년 전
부모
커밋
3e47a4f664

+ 3 - 3
README.md

@@ -366,7 +366,7 @@ SFTPGo makes use of the third party libraries listed inside [go.mod](./go.mod).
 
 We are very grateful to all the people who contributed with ideas and/or pull requests.
 
-Thank you [ysura](https://www.ysura.com/) for granting us stable access to a test AWS S3 account.
+Thank you to [ysura](https://www.ysura.com/) for granting us stable access to a test AWS S3 account.
 
 Thank you to [KeenThemes](https://keenthemes.com/) for granting us a custom license to use their amazing [Mega Bundle](https://keenthemes.com/products/templates-mega-bundle) for SFTPGo UI.
 
@@ -374,7 +374,7 @@ Thank you to [KeenThemes](https://keenthemes.com/) for granting us a custom lice
 
 GNU AGPL-3.0-only
 
-The [theme](https://keenthemes.com/products/templates-mega-bundle) used in WebClient UI is proprietary, this means:
+The [theme](https://keenthemes.com/products/templates-mega-bundle) used in WebAdmin and WebClient user interfaces is proprietary, this means:
 
 - KeenThemes HTML/CSS/JS components are allowed for use only within the SFTPGo product and restricted to be used in a resealable HTML template that can compete with KeenThemes products anyhow.
-- The SFTPGo WebClient UI (HTML, CSS and JS components) based on this theme is allowed for use only within the SFTPGo product and therefore cannot be used in derivative works/products without an explicit grant from the [SFTPGo Team](mailto:[email protected]).
+- The SFTPGo WebAdmin and WebClient user interfaces (HTML, CSS and JS components) based on this theme are allowed for use only within the SFTPGo product and therefore cannot be used in derivative works/products without an explicit grant from the [SFTPGo Team](mailto:[email protected]).

+ 1 - 1
internal/httpd/api_admin.go

@@ -262,7 +262,7 @@ func resetAdminPassword(w http.ResponseWriter, r *http.Request) {
 		sendAPIResponse(w, r, err, "", http.StatusBadRequest)
 		return
 	}
-	_, _, err = handleResetPassword(r, req.Code, req.Password, true)
+	_, _, err = handleResetPassword(r, req.Code, req.Password, req.Password, true)
 	if err != nil {
 		sendAPIResponse(w, r, err, "", getRespStatus(err))
 		return

+ 1 - 1
internal/httpd/api_user.go

@@ -243,7 +243,7 @@ func resetUserPassword(w http.ResponseWriter, r *http.Request) {
 		sendAPIResponse(w, r, err, "", http.StatusBadRequest)
 		return
 	}
-	_, _, err = handleResetPassword(r, req.Code, req.Password, false)
+	_, _, err = handleResetPassword(r, req.Code, req.Password, req.Password, false)
 	if err != nil {
 		sendAPIResponse(w, r, err, "", getRespStatus(err))
 		return

+ 5 - 1
internal/httpd/api_utils.go

@@ -721,7 +721,7 @@ func handleForgotPassword(r *http.Request, username string, isAdmin bool) error
 	return resetCodesMgr.Add(c)
 }
 
-func handleResetPassword(r *http.Request, code, newPassword string, isAdmin bool) (
+func handleResetPassword(r *http.Request, code, newPassword, confirmPassword string, isAdmin bool) (
 	*dataprovider.Admin, *dataprovider.User, error,
 ) {
 	var admin dataprovider.Admin
@@ -734,6 +734,10 @@ func handleResetPassword(r *http.Request, code, newPassword string, isAdmin bool
 	if code == "" {
 		return &admin, &user, util.NewValidationError("please set a confirmation code")
 	}
+	if newPassword != confirmPassword {
+		return &admin, &user, util.NewI18nError(errors.New("the two password fields do not match"), util.I18nErrorChangePwdNoMatch)
+	}
+
 	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
 	resetCode, err := resetCodesMgr.Get(code)
 	if err != nil {

+ 3 - 7
internal/httpd/httpd.go

@@ -441,13 +441,9 @@ func (b *UIBranding) check(isWebClient bool) {
 			b.DefaultCSS[idx] = util.CleanPath(b.DefaultCSS[idx])
 		}
 	} else {
-		if isWebClient {
-			b.DefaultCSS = []string{
-				"/assets/plugins/global/plugins.bundle.css",
-				"/assets/css/style.bundle.css",
-			}
-		} else {
-			b.DefaultCSS = []string{"/css/sb-admin-2.min.css"}
+		b.DefaultCSS = []string{
+			"/assets/plugins/global/plugins.bundle.css",
+			"/assets/css/style.bundle.css",
 		}
 	}
 	for idx := range b.ExtraCSS {

+ 56 - 49
internal/httpd/server.go

@@ -161,7 +161,7 @@ func (s *httpdServer) refreshCookie(next http.Handler) http.Handler {
 }
 
 func (s *httpdServer) renderClientLoginPage(w http.ResponseWriter, r *http.Request, err *util.I18nError, ip string) {
-	data := clientLoginPage{
+	data := loginPage{
 		commonBasePage: getCommonBasePage(r),
 		Title:          util.I18nLoginTitle,
 		CurrentURL:     webClientLoginPath,
@@ -183,7 +183,7 @@ func (s *httpdServer) renderClientLoginPage(w http.ResponseWriter, r *http.Reque
 	if s.binding.OIDC.isEnabled() && !s.binding.isWebClientOIDCLoginDisabled() {
 		data.OpenIDLoginURL = webClientOIDCLoginPath
 	}
-	renderClientTemplate(w, templateClientLogin, data)
+	renderClientTemplate(w, templateCommonLogin, data)
 }
 
 func (s *httpdServer) handleWebClientLogout(w http.ResponseWriter, r *http.Request) {
@@ -296,14 +296,8 @@ func (s *httpdServer) handleWebClientPasswordResetPost(w http.ResponseWriter, r
 	}
 	newPassword := strings.TrimSpace(r.Form.Get("password"))
 	confirmPassword := strings.TrimSpace(r.Form.Get("confirm_password"))
-	if newPassword != confirmPassword {
-		s.renderClientResetPwdPage(w, r, util.NewI18nError(
-			errors.New("the two password fields do not match"),
-			util.I18nErrorChangePwdNoMatch), ipAddr)
-		return
-	}
 	_, user, err := handleResetPassword(r, strings.TrimSpace(r.Form.Get("code")),
-		newPassword, false)
+		newPassword, confirmPassword, false)
 	if err != nil {
 		s.renderClientResetPwdPage(w, r, util.NewI18nError(err, util.I18nErrorChangePwdGeneric), ipAddr)
 		return
@@ -457,17 +451,18 @@ func (s *httpdServer) handleWebAdminTwoFactorRecoveryPost(w http.ResponseWriter,
 	}
 	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
 	if err := r.ParseForm(); err != nil {
-		s.renderTwoFactorRecoveryPage(w, r, err.Error(), ipAddr)
+		s.renderTwoFactorRecoveryPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm), ipAddr)
 		return
 	}
 	username := claims.Username
 	recoveryCode := strings.TrimSpace(r.Form.Get("recovery_code"))
 	if username == "" || recoveryCode == "" {
-		s.renderTwoFactorRecoveryPage(w, r, "Invalid credentials", ipAddr)
+		s.renderTwoFactorRecoveryPage(w, r, util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials),
+			ipAddr)
 		return
 	}
 	if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
-		s.renderTwoFactorRecoveryPage(w, r, err.Error(), ipAddr)
+		s.renderTwoFactorRecoveryPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF), ipAddr)
 		return
 	}
 	admin, err := dataprovider.AdminExists(username)
@@ -475,11 +470,12 @@ func (s *httpdServer) handleWebAdminTwoFactorRecoveryPost(w http.ResponseWriter,
 		if errors.Is(err, util.ErrNotFound) {
 			handleDefenderEventLoginFailed(ipAddr, err) //nolint:errcheck
 		}
-		s.renderTwoFactorRecoveryPage(w, r, "Invalid credentials", ipAddr)
+		s.renderTwoFactorRecoveryPage(w, r, util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials),
+			ipAddr)
 		return
 	}
 	if !admin.Filters.TOTPConfig.Enabled {
-		s.renderTwoFactorRecoveryPage(w, r, "Two factory authentication is not enabled", ipAddr)
+		s.renderTwoFactorRecoveryPage(w, r, util.NewI18nError(util.NewValidationError("two factory authentication is not enabled"), util.I18n2FADisabled), ipAddr)
 		return
 	}
 	for idx, code := range admin.Filters.RecoveryCodes {
@@ -489,7 +485,8 @@ func (s *httpdServer) handleWebAdminTwoFactorRecoveryPost(w http.ResponseWriter,
 		}
 		if code.Secret.GetPayload() == recoveryCode {
 			if code.Used {
-				s.renderTwoFactorRecoveryPage(w, r, "This recovery code was already used", ipAddr)
+				s.renderTwoFactorRecoveryPage(w, r,
+					util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials), ipAddr)
 				return
 			}
 			admin.Filters.RecoveryCodes[idx].Used = true
@@ -504,7 +501,8 @@ func (s *httpdServer) handleWebAdminTwoFactorRecoveryPost(w http.ResponseWriter,
 		}
 	}
 	handleDefenderEventLoginFailed(ipAddr, dataprovider.ErrInvalidCredentials) //nolint:errcheck
-	s.renderTwoFactorRecoveryPage(w, r, "Invalid recovery code", ipAddr)
+	s.renderTwoFactorRecoveryPage(w, r, util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials),
+		ipAddr)
 }
 
 func (s *httpdServer) handleWebAdminTwoFactorPost(w http.ResponseWriter, r *http.Request) {
@@ -516,18 +514,19 @@ func (s *httpdServer) handleWebAdminTwoFactorPost(w http.ResponseWriter, r *http
 	}
 	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
 	if err := r.ParseForm(); err != nil {
-		s.renderTwoFactorPage(w, r, err.Error(), ipAddr)
+		s.renderTwoFactorPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm), ipAddr)
 		return
 	}
 	username := claims.Username
 	passcode := strings.TrimSpace(r.Form.Get("passcode"))
 	if username == "" || passcode == "" {
-		s.renderTwoFactorPage(w, r, "Invalid credentials", ipAddr)
+		s.renderTwoFactorPage(w, r, util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials),
+			ipAddr)
 		return
 	}
 	if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
 		err = handleDefenderEventLoginFailed(ipAddr, err)
-		s.renderTwoFactorPage(w, r, err.Error(), ipAddr)
+		s.renderTwoFactorPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF), ipAddr)
 		return
 	}
 	admin, err := dataprovider.AdminExists(username)
@@ -535,11 +534,11 @@ func (s *httpdServer) handleWebAdminTwoFactorPost(w http.ResponseWriter, r *http
 		if errors.Is(err, util.ErrNotFound) {
 			handleDefenderEventLoginFailed(ipAddr, err) //nolint:errcheck
 		}
-		s.renderTwoFactorPage(w, r, "Invalid credentials", ipAddr)
+		s.renderTwoFactorPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCredentials), ipAddr)
 		return
 	}
 	if !admin.Filters.TOTPConfig.Enabled {
-		s.renderTwoFactorPage(w, r, "Two factory authentication is not enabled", ipAddr)
+		s.renderTwoFactorPage(w, r, util.NewI18nError(common.ErrInternalFailure, util.I18n2FADisabled), ipAddr)
 		return
 	}
 	err = admin.Filters.TOTPConfig.Secret.Decrypt()
@@ -551,7 +550,8 @@ func (s *httpdServer) handleWebAdminTwoFactorPost(w http.ResponseWriter, r *http
 		admin.Filters.TOTPConfig.Secret.GetPayload())
 	if !match || err != nil {
 		handleDefenderEventLoginFailed(ipAddr, dataprovider.ErrInvalidCredentials) //nolint:errcheck
-		s.renderTwoFactorPage(w, r, "Invalid authentication code", ipAddr)
+		s.renderTwoFactorPage(w, r, util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials),
+			ipAddr)
 		return
 	}
 	s.loginAdmin(w, r, &admin, true, s.renderTwoFactorPage, ipAddr)
@@ -562,34 +562,36 @@ func (s *httpdServer) handleWebAdminLoginPost(w http.ResponseWriter, r *http.Req
 
 	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
 	if err := r.ParseForm(); err != nil {
-		s.renderAdminLoginPage(w, r, err.Error(), ipAddr)
+		s.renderAdminLoginPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm), ipAddr)
 		return
 	}
 	username := strings.TrimSpace(r.Form.Get("username"))
 	password := strings.TrimSpace(r.Form.Get("password"))
 	if username == "" || password == "" {
-		s.renderAdminLoginPage(w, r, "Invalid credentials", ipAddr)
+		s.renderAdminLoginPage(w, r, util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials),
+			ipAddr)
 		return
 	}
 	if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
-		s.renderAdminLoginPage(w, r, err.Error(), ipAddr)
+		s.renderAdminLoginPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF), ipAddr)
 		return
 	}
 	admin, err := dataprovider.CheckAdminAndPass(username, password, ipAddr)
 	if err != nil {
-		err = handleDefenderEventLoginFailed(ipAddr, err)
-		s.renderAdminLoginPage(w, r, err.Error(), ipAddr)
+		handleDefenderEventLoginFailed(ipAddr, err)
+		s.renderAdminLoginPage(w, r, util.NewI18nError(dataprovider.ErrInvalidCredentials, util.I18nErrorInvalidCredentials),
+			ipAddr)
 		return
 	}
 	s.loginAdmin(w, r, &admin, false, s.renderAdminLoginPage, ipAddr)
 }
 
-func (s *httpdServer) renderAdminLoginPage(w http.ResponseWriter, r *http.Request, error, ip string) {
+func (s *httpdServer) renderAdminLoginPage(w http.ResponseWriter, r *http.Request, err *util.I18nError, ip string) {
 	data := loginPage{
 		commonBasePage: getCommonBasePage(r),
 		Title:          util.I18nLoginTitle,
 		CurrentURL:     webAdminLoginPath,
-		Error:          error,
+		Error:          err,
 		CSRFToken:      createCSRFToken(ip),
 		Branding:       s.binding.Branding.WebAdmin,
 		FormDisabled:   s.binding.isWebAdminLoginFormDisabled(),
@@ -604,7 +606,7 @@ func (s *httpdServer) renderAdminLoginPage(w http.ResponseWriter, r *http.Reques
 	if s.binding.OIDC.hasRoles() && !s.binding.isWebAdminOIDCLoginDisabled() {
 		data.OpenIDLoginURL = webAdminOIDCLoginPath
 	}
-	renderAdminTemplate(w, templateLogin, data)
+	renderAdminTemplate(w, templateCommonLogin, data)
 }
 
 func (s *httpdServer) handleWebAdminLogin(w http.ResponseWriter, r *http.Request) {
@@ -613,7 +615,8 @@ func (s *httpdServer) handleWebAdminLogin(w http.ResponseWriter, r *http.Request
 		http.Redirect(w, r, webAdminSetupPath, http.StatusFound)
 		return
 	}
-	s.renderAdminLoginPage(w, r, getFlashMessage(w, r).ErrorString, util.GetIPFromRemoteAddress(r.RemoteAddr))
+	msg := getFlashMessage(w, r)
+	s.renderAdminLoginPage(w, r, msg.getI18nError(), util.GetIPFromRemoteAddress(r.RemoteAddr))
 }
 
 func (s *httpdServer) handleWebAdminLogout(w http.ResponseWriter, r *http.Request) {
@@ -651,22 +654,19 @@ func (s *httpdServer) handleWebAdminPasswordResetPost(w http.ResponseWriter, r *
 	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
 	err := r.ParseForm()
 	if err != nil {
-		s.renderResetPwdPage(w, r, err.Error(), ipAddr)
+		s.renderResetPwdPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm), ipAddr)
 		return
 	}
 	if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
 		s.renderForbiddenPage(w, r, err.Error())
 		return
 	}
+	newPassword := strings.TrimSpace(r.Form.Get("password"))
+	confirmPassword := strings.TrimSpace(r.Form.Get("confirm_password"))
 	admin, _, err := handleResetPassword(r, strings.TrimSpace(r.Form.Get("code")),
-		strings.TrimSpace(r.Form.Get("password")), true)
+		newPassword, confirmPassword, true)
 	if err != nil {
-		var e *util.ValidationError
-		if errors.As(err, &e) {
-			s.renderResetPwdPage(w, r, e.GetErrorString(), ipAddr)
-			return
-		}
-		s.renderResetPwdPage(w, r, err.Error(), ipAddr)
+		s.renderResetPwdPage(w, r, util.NewI18nError(err, util.I18nErrorChangePwdGeneric), ipAddr)
 		return
 	}
 
@@ -679,12 +679,12 @@ func (s *httpdServer) handleWebAdminSetupPost(w http.ResponseWriter, r *http.Req
 		s.renderBadRequestPage(w, r, errors.New("an admin user already exists"))
 		return
 	}
+	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
 	err := r.ParseForm()
 	if err != nil {
-		s.renderAdminSetupPage(w, r, "", err.Error())
+		s.renderAdminSetupPage(w, r, "", ipAddr, util.NewI18nError(err, util.I18nErrorInvalidForm))
 		return
 	}
-	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
 	if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
 		s.renderForbiddenPage(w, r, err.Error())
 		return
@@ -694,19 +694,26 @@ func (s *httpdServer) handleWebAdminSetupPost(w http.ResponseWriter, r *http.Req
 	confirmPassword := strings.TrimSpace(r.Form.Get("confirm_password"))
 	installCode := strings.TrimSpace(r.Form.Get("install_code"))
 	if installationCode != "" && installCode != resolveInstallationCode() {
-		s.renderAdminSetupPage(w, r, username, fmt.Sprintf("%v mismatch", installationCodeHint))
+		s.renderAdminSetupPage(w, r, username, ipAddr,
+			util.NewI18nError(
+				util.NewValidationError(fmt.Sprintf("%v mismatch", installationCodeHint)),
+				util.I18nErrorSetupInstallCode),
+		)
 		return
 	}
 	if username == "" {
-		s.renderAdminSetupPage(w, r, username, "Please set a username")
+		s.renderAdminSetupPage(w, r, username, ipAddr,
+			util.NewI18nError(util.NewValidationError("please set a username"), util.I18nError500Message))
 		return
 	}
 	if password == "" {
-		s.renderAdminSetupPage(w, r, username, "Please set a password")
+		s.renderAdminSetupPage(w, r, username, ipAddr,
+			util.NewI18nError(util.NewValidationError("please set a password"), util.I18nError500Message))
 		return
 	}
 	if password != confirmPassword {
-		s.renderAdminSetupPage(w, r, username, "Passwords mismatch")
+		s.renderAdminSetupPage(w, r, username, ipAddr,
+			util.NewI18nError(errors.New("the two password fields do not match"), util.I18nErrorChangePwdNoMatch))
 		return
 	}
 	admin := dataprovider.Admin{
@@ -717,7 +724,7 @@ func (s *httpdServer) handleWebAdminSetupPost(w http.ResponseWriter, r *http.Req
 	}
 	err = dataprovider.AddAdmin(&admin, username, ipAddr, "")
 	if err != nil {
-		s.renderAdminSetupPage(w, r, username, err.Error())
+		s.renderAdminSetupPage(w, r, username, ipAddr, util.NewI18nError(err, util.I18nError500Message))
 		return
 	}
 	s.loginAdmin(w, r, &admin, false, nil, ipAddr)
@@ -772,7 +779,7 @@ func (s *httpdServer) loginUser(
 
 func (s *httpdServer) loginAdmin(
 	w http.ResponseWriter, r *http.Request, admin *dataprovider.Admin,
-	isSecondFactorAuth bool, errorFunc func(w http.ResponseWriter, r *http.Request, error, ip string),
+	isSecondFactorAuth bool, errorFunc func(w http.ResponseWriter, r *http.Request, err *util.I18nError, ip string),
 	ipAddr string,
 ) {
 	c := jwtTokenClaims{
@@ -792,10 +799,10 @@ func (s *httpdServer) loginAdmin(
 	if err != nil {
 		logger.Warn(logSender, "", "unable to set admin login cookie %v", err)
 		if errorFunc == nil {
-			s.renderAdminSetupPage(w, r, admin.Username, err.Error())
+			s.renderAdminSetupPage(w, r, admin.Username, ipAddr, util.NewI18nError(err, util.I18nError500Message))
 			return
 		}
-		errorFunc(w, r, err.Error(), ipAddr)
+		errorFunc(w, r, util.NewI18nError(err, util.I18nError500Message), ipAddr)
 		return
 	}
 	if isSecondFactorAuth {

+ 5 - 4
internal/httpd/web.go

@@ -49,6 +49,7 @@ const (
 	templateCommonCSS          = "sftpgo.css"
 	templateCommonBase         = "base.html"
 	templateCommonBaseLogin    = "baselogin.html"
+	templateCommonLogin        = "login.html"
 )
 
 var (
@@ -64,7 +65,7 @@ type commonBasePage struct {
 type loginPage struct {
 	commonBasePage
 	CurrentURL     string
-	Error          string
+	Error          *util.I18nError
 	CSRFToken      string
 	AltLoginURL    string
 	AltLoginName   string
@@ -78,7 +79,7 @@ type loginPage struct {
 type twoFactorPage struct {
 	commonBasePage
 	CurrentURL  string
-	Error       string
+	Error       *util.I18nError
 	CSRFToken   string
 	RecoveryURL string
 	Title       string
@@ -88,7 +89,7 @@ type twoFactorPage struct {
 type forgotPwdPage struct {
 	commonBasePage
 	CurrentURL string
-	Error      string
+	Error      *util.I18nError
 	CSRFToken  string
 	LoginURL   string
 	Title      string
@@ -98,7 +99,7 @@ type forgotPwdPage struct {
 type resetPwdPage struct {
 	commonBasePage
 	CurrentURL string
-	Error      string
+	Error      *util.I18nError
 	CSRFToken  string
 	LoginURL   string
 	Title      string

+ 48 - 45
internal/httpd/webadmin.go

@@ -72,7 +72,6 @@ const (
 const (
 	templateAdminDir         = "webadmin"
 	templateBase             = "base.html"
-	templateBaseLogin        = "baselogin.html"
 	templateFsConfig         = "fsconfig.html"
 	templateSharedComponents = "sharedcomponents.html"
 	templateUsers            = "users.html"
@@ -93,7 +92,6 @@ const (
 	templateEvents           = "events.html"
 	templateMessage          = "message.html"
 	templateStatus           = "status.html"
-	templateLogin            = "login.html"
 	templateDefender         = "defender.html"
 	templateIPLists          = "iplists.html"
 	templateIPList           = "iplist.html"
@@ -119,8 +117,6 @@ const (
 	pageIPListsTitle         = "IP Lists"
 	pageEventsTitle          = "Logs"
 	pageConfigsTitle         = "Configurations"
-	pageForgotPwdTitle       = "Forgot password"
-	pageResetPwdTitle        = "Reset password"
 	pageSetupTitle           = "Create first admin user"
 	defaultQueryLimit        = 1000
 	inversePatternType       = "inverse"
@@ -323,12 +319,16 @@ type ipListPage struct {
 }
 
 type setupPage struct {
-	basePage
+	commonBasePage
+	CurrentURL           string
+	Error                *util.I18nError
+	CSRFToken            string
 	Username             string
 	HasInstallationCode  bool
 	InstallationCodeHint string
 	HideSupportLink      bool
-	Error                string
+	Title                string
+	Branding             UIBranding
 }
 
 type folderPage struct {
@@ -506,9 +506,9 @@ func loadAdminTemplates(templatesPath string) {
 		filepath.Join(templatesPath, templateAdminDir, templateStatus),
 	}
 	loginPaths := []string{
-		filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
-		filepath.Join(templatesPath, templateAdminDir, templateBaseLogin),
-		filepath.Join(templatesPath, templateAdminDir, templateLogin),
+		filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
+		filepath.Join(templatesPath, templateCommonDir, templateCommonBaseLogin),
+		filepath.Join(templatesPath, templateCommonDir, templateCommonLogin),
 	}
 	maintenancePaths := []string{
 		filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
@@ -536,26 +536,28 @@ func loadAdminTemplates(templatesPath string) {
 		filepath.Join(templatesPath, templateAdminDir, templateMFA),
 	}
 	twoFactorPaths := []string{
-		filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
-		filepath.Join(templatesPath, templateAdminDir, templateBaseLogin),
-		filepath.Join(templatesPath, templateAdminDir, templateTwoFactor),
+		filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
+		filepath.Join(templatesPath, templateCommonDir, templateCommonBaseLogin),
+		filepath.Join(templatesPath, templateCommonDir, templateTwoFactor),
 	}
 	twoFactorRecoveryPaths := []string{
-		filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
-		filepath.Join(templatesPath, templateAdminDir, templateBaseLogin),
-		filepath.Join(templatesPath, templateAdminDir, templateTwoFactorRecovery),
+		filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
+		filepath.Join(templatesPath, templateCommonDir, templateCommonBaseLogin),
+		filepath.Join(templatesPath, templateCommonDir, templateTwoFactorRecovery),
 	}
 	setupPaths := []string{
-		filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
-		filepath.Join(templatesPath, templateAdminDir, templateBaseLogin),
+		filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
+		filepath.Join(templatesPath, templateCommonDir, templateCommonBaseLogin),
 		filepath.Join(templatesPath, templateAdminDir, templateSetup),
 	}
 	forgotPwdPaths := []string{
-		filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
+		filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
+		filepath.Join(templatesPath, templateCommonDir, templateCommonBaseLogin),
 		filepath.Join(templatesPath, templateCommonDir, templateForgotPassword),
 	}
 	resetPwdPaths := []string{
-		filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
+		filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
+		filepath.Join(templatesPath, templateCommonDir, templateCommonBaseLogin),
 		filepath.Join(templatesPath, templateCommonDir, templateResetPassword),
 	}
 	rolesPaths := []string{
@@ -636,7 +638,7 @@ func loadAdminTemplates(templatesPath string) {
 	adminTemplates[templateEventActions] = eventActionsTmpl
 	adminTemplates[templateEventAction] = eventActionTmpl
 	adminTemplates[templateStatus] = statusTmpl
-	adminTemplates[templateLogin] = loginTmpl
+	adminTemplates[templateCommonLogin] = loginTmpl
 	adminTemplates[templateProfile] = profileTmpl
 	adminTemplates[templateChangePwd] = changePwdTmpl
 	adminTemplates[templateMaintenance] = maintenanceTmpl
@@ -797,36 +799,38 @@ func (s *httpdServer) renderNotFoundPage(w http.ResponseWriter, r *http.Request,
 	s.renderMessagePage(w, r, page404Title, page404Body, http.StatusNotFound, err, "")
 }
 
-func (s *httpdServer) renderForgotPwdPage(w http.ResponseWriter, r *http.Request, error, ip string) {
+func (s *httpdServer) renderForgotPwdPage(w http.ResponseWriter, r *http.Request, err *util.I18nError, ip string) {
 	data := forgotPwdPage{
 		commonBasePage: getCommonBasePage(r),
 		CurrentURL:     webAdminForgotPwdPath,
-		Error:          error,
+		Error:          err,
 		CSRFToken:      createCSRFToken(ip),
-		Title:          pageForgotPwdTitle,
+		LoginURL:       webAdminLoginPath,
+		Title:          util.I18nForgotPwdTitle,
 		Branding:       s.binding.Branding.WebAdmin,
 	}
 	renderAdminTemplate(w, templateForgotPassword, data)
 }
 
-func (s *httpdServer) renderResetPwdPage(w http.ResponseWriter, r *http.Request, error, ip string) {
+func (s *httpdServer) renderResetPwdPage(w http.ResponseWriter, r *http.Request, err *util.I18nError, ip string) {
 	data := resetPwdPage{
 		commonBasePage: getCommonBasePage(r),
 		CurrentURL:     webAdminResetPwdPath,
-		Error:          error,
+		Error:          err,
 		CSRFToken:      createCSRFToken(ip),
-		Title:          pageResetPwdTitle,
+		LoginURL:       webAdminLoginPath,
+		Title:          util.I18nResetPwdTitle,
 		Branding:       s.binding.Branding.WebAdmin,
 	}
 	renderAdminTemplate(w, templateResetPassword, data)
 }
 
-func (s *httpdServer) renderTwoFactorPage(w http.ResponseWriter, r *http.Request, error, ip string) {
+func (s *httpdServer) renderTwoFactorPage(w http.ResponseWriter, r *http.Request, err *util.I18nError, ip string) {
 	data := twoFactorPage{
 		commonBasePage: getCommonBasePage(r),
 		Title:          pageTwoFactorTitle,
 		CurrentURL:     webAdminTwoFactorPath,
-		Error:          error,
+		Error:          err,
 		CSRFToken:      createCSRFToken(ip),
 		RecoveryURL:    webAdminTwoFactorRecoveryPath,
 		Branding:       s.binding.Branding.WebAdmin,
@@ -834,12 +838,12 @@ func (s *httpdServer) renderTwoFactorPage(w http.ResponseWriter, r *http.Request
 	renderAdminTemplate(w, templateTwoFactor, data)
 }
 
-func (s *httpdServer) renderTwoFactorRecoveryPage(w http.ResponseWriter, r *http.Request, error, ip string) {
+func (s *httpdServer) renderTwoFactorRecoveryPage(w http.ResponseWriter, r *http.Request, err *util.I18nError, ip string) {
 	data := twoFactorPage{
 		commonBasePage: getCommonBasePage(r),
 		Title:          pageTwoFactorRecoveryTitle,
 		CurrentURL:     webAdminTwoFactorRecoveryPath,
-		Error:          error,
+		Error:          err,
 		CSRFToken:      createCSRFToken(ip),
 		Branding:       s.binding.Branding.WebAdmin,
 	}
@@ -926,14 +930,18 @@ func (s *httpdServer) renderConfigsPage(w http.ResponseWriter, r *http.Request,
 	renderAdminTemplate(w, templateConfigs, data)
 }
 
-func (s *httpdServer) renderAdminSetupPage(w http.ResponseWriter, r *http.Request, username, error string) {
+func (s *httpdServer) renderAdminSetupPage(w http.ResponseWriter, r *http.Request, username, ip string, err *util.I18nError) {
 	data := setupPage{
-		basePage:             s.getBasePageData(pageSetupTitle, webAdminSetupPath, r),
+		commonBasePage:       getCommonBasePage(r),
+		Title:                util.I18nSetupTitle,
+		CurrentURL:           webAdminSetupPath,
+		CSRFToken:            createCSRFToken(ip),
 		Username:             username,
 		HasInstallationCode:  installationCode != "",
 		InstallationCodeHint: installationCodeHint,
 		HideSupportLink:      hideSupportLink,
-		Error:                error,
+		Error:                err,
+		Branding:             s.binding.Branding.WebAdmin,
 	}
 
 	renderAdminTemplate(w, templateSetup, data)
@@ -2634,7 +2642,7 @@ func (s *httpdServer) handleWebAdminForgotPwd(w http.ResponseWriter, r *http.Req
 		s.renderNotFoundPage(w, r, errors.New("this page does not exist"))
 		return
 	}
-	s.renderForgotPwdPage(w, r, "", util.GetIPFromRemoteAddress(r.RemoteAddr))
+	s.renderForgotPwdPage(w, r, nil, util.GetIPFromRemoteAddress(r.RemoteAddr))
 }
 
 func (s *httpdServer) handleWebAdminForgotPwdPost(w http.ResponseWriter, r *http.Request) {
@@ -2643,7 +2651,7 @@ func (s *httpdServer) handleWebAdminForgotPwdPost(w http.ResponseWriter, r *http
 	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
 	err := r.ParseForm()
 	if err != nil {
-		s.renderForgotPwdPage(w, r, err.Error(), ipAddr)
+		s.renderForgotPwdPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm), ipAddr)
 		return
 	}
 	if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
@@ -2652,12 +2660,7 @@ func (s *httpdServer) handleWebAdminForgotPwdPost(w http.ResponseWriter, r *http
 	}
 	err = handleForgotPassword(r, r.Form.Get("username"), true)
 	if err != nil {
-		var e *util.ValidationError
-		if errors.As(err, &e) {
-			s.renderForgotPwdPage(w, r, e.GetErrorString(), ipAddr)
-			return
-		}
-		s.renderForgotPwdPage(w, r, err.Error(), ipAddr)
+		s.renderForgotPwdPage(w, r, util.NewI18nError(err, util.I18nErrorPwdResetGeneric), ipAddr)
 		return
 	}
 	http.Redirect(w, r, webAdminResetPwdPath, http.StatusFound)
@@ -2669,17 +2672,17 @@ func (s *httpdServer) handleWebAdminPasswordReset(w http.ResponseWriter, r *http
 		s.renderNotFoundPage(w, r, errors.New("this page does not exist"))
 		return
 	}
-	s.renderResetPwdPage(w, r, "", util.GetIPFromRemoteAddress(r.RemoteAddr))
+	s.renderResetPwdPage(w, r, nil, util.GetIPFromRemoteAddress(r.RemoteAddr))
 }
 
 func (s *httpdServer) handleWebAdminTwoFactor(w http.ResponseWriter, r *http.Request) {
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
-	s.renderTwoFactorPage(w, r, "", util.GetIPFromRemoteAddress(r.RemoteAddr))
+	s.renderTwoFactorPage(w, r, nil, util.GetIPFromRemoteAddress(r.RemoteAddr))
 }
 
 func (s *httpdServer) handleWebAdminTwoFactorRecovery(w http.ResponseWriter, r *http.Request) {
 	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
-	s.renderTwoFactorRecoveryPage(w, r, "", util.GetIPFromRemoteAddress(r.RemoteAddr))
+	s.renderTwoFactorRecoveryPage(w, r, nil, util.GetIPFromRemoteAddress(r.RemoteAddr))
 }
 
 func (s *httpdServer) handleWebAdminMFA(w http.ResponseWriter, r *http.Request) {
@@ -2824,7 +2827,7 @@ func (s *httpdServer) handleWebAdminSetupGet(w http.ResponseWriter, r *http.Requ
 		http.Redirect(w, r, webAdminLoginPath, http.StatusFound)
 		return
 	}
-	s.renderAdminSetupPage(w, r, "", "")
+	s.renderAdminSetupPage(w, r, "", util.GetIPFromRemoteAddress(r.RemoteAddr), nil)
 }
 
 func (s *httpdServer) handleWebAddAdminGet(w http.ResponseWriter, r *http.Request) {

+ 44 - 92
internal/httpd/webclient.go

@@ -45,23 +45,20 @@ import (
 )
 
 const (
-	templateClientDir               = "webclient"
-	templateClientBase              = "base.html"
-	templateClientLogin             = "login.html"
-	templateClientFiles             = "files.html"
-	templateClientMessage           = "message.html"
-	templateClientProfile           = "profile.html"
-	templateClientChangePwd         = "changepassword.html"
-	templateClientTwoFactor         = "twofactor.html"
-	templateClientTwoFactorRecovery = "twofactor-recovery.html"
-	templateClientMFA               = "mfa.html"
-	templateClientEditFile          = "editfile.html"
-	templateClientShare             = "share.html"
-	templateClientShares            = "shares.html"
-	templateClientViewPDF           = "viewpdf.html"
-	templateShareLogin              = "sharelogin.html"
-	templateShareDownload           = "sharedownload.html"
-	templateUploadToShare           = "shareupload.html"
+	templateClientDir       = "webclient"
+	templateClientBase      = "base.html"
+	templateClientFiles     = "files.html"
+	templateClientMessage   = "message.html"
+	templateClientProfile   = "profile.html"
+	templateClientChangePwd = "changepassword.html"
+	templateClientMFA       = "mfa.html"
+	templateClientEditFile  = "editfile.html"
+	templateClientShare     = "share.html"
+	templateClientShares    = "shares.html"
+	templateClientViewPDF   = "viewpdf.html"
+	templateShareLogin      = "sharelogin.html"
+	templateShareDownload   = "sharedownload.html"
+	templateUploadToShare   = "shareupload.html"
 )
 
 // condResult is the result of an HTTP request precondition check.
@@ -212,51 +209,6 @@ type clientSharePage struct {
 	IsAdd bool
 }
 
-// TODO: merge with loginPage once the WebAdmin supports localization
-type clientLoginPage struct {
-	commonBasePage
-	CurrentURL     string
-	Error          *util.I18nError
-	CSRFToken      string
-	AltLoginURL    string
-	AltLoginName   string
-	ForgotPwdURL   string
-	OpenIDLoginURL string
-	Title          string
-	Branding       UIBranding
-	FormDisabled   bool
-}
-
-type clientResetPwdPage struct {
-	commonBasePage
-	CurrentURL string
-	Error      *util.I18nError
-	CSRFToken  string
-	LoginURL   string
-	Title      string
-	Branding   UIBranding
-}
-
-type clientTwoFactorPage struct {
-	commonBasePage
-	CurrentURL  string
-	Error       *util.I18nError
-	CSRFToken   string
-	RecoveryURL string
-	Title       string
-	Branding    UIBranding
-}
-
-type clientForgotPwdPage struct {
-	commonBasePage
-	CurrentURL string
-	Error      *util.I18nError
-	CSRFToken  string
-	LoginURL   string
-	Title      string
-	Branding   UIBranding
-}
-
 type userQuotaUsage struct {
 	QuotaSize                int64
 	QuotaFiles               int
@@ -478,40 +430,40 @@ func loadClientTemplates(templatesPath string) {
 		filepath.Join(templatesPath, templateClientDir, templateClientBase),
 		filepath.Join(templatesPath, templateClientDir, templateClientChangePwd),
 	}
-	loginPath := []string{
+	loginPaths := []string{
 		filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
-		filepath.Join(templatesPath, templateCommonDir, templateBaseLogin),
-		filepath.Join(templatesPath, templateClientDir, templateClientLogin),
+		filepath.Join(templatesPath, templateCommonDir, templateCommonBaseLogin),
+		filepath.Join(templatesPath, templateCommonDir, templateCommonLogin),
 	}
-	messagePath := []string{
+	messagePaths := []string{
 		filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
 		filepath.Join(templatesPath, templateClientDir, templateClientBase),
 		filepath.Join(templatesPath, templateClientDir, templateClientMessage),
 	}
-	mfaPath := []string{
+	mfaPaths := []string{
 		filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
 		filepath.Join(templatesPath, templateClientDir, templateClientBase),
 		filepath.Join(templatesPath, templateClientDir, templateClientMFA),
 	}
-	twoFactorPath := []string{
+	twoFactorPaths := []string{
 		filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
-		filepath.Join(templatesPath, templateCommonDir, templateBaseLogin),
-		filepath.Join(templatesPath, templateClientDir, templateClientTwoFactor),
+		filepath.Join(templatesPath, templateCommonDir, templateCommonBaseLogin),
+		filepath.Join(templatesPath, templateCommonDir, templateTwoFactor),
 	}
-	twoFactorRecoveryPath := []string{
+	twoFactorRecoveryPaths := []string{
 		filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
-		filepath.Join(templatesPath, templateCommonDir, templateBaseLogin),
-		filepath.Join(templatesPath, templateClientDir, templateClientTwoFactorRecovery),
+		filepath.Join(templatesPath, templateCommonDir, templateCommonBaseLogin),
+		filepath.Join(templatesPath, templateCommonDir, templateTwoFactorRecovery),
 	}
 	forgotPwdPaths := []string{
 		filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
-		filepath.Join(templatesPath, templateCommonDir, templateBaseLogin),
-		filepath.Join(templatesPath, templateClientDir, templateForgotPassword),
+		filepath.Join(templatesPath, templateCommonDir, templateCommonBaseLogin),
+		filepath.Join(templatesPath, templateCommonDir, templateForgotPassword),
 	}
 	resetPwdPaths := []string{
 		filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
-		filepath.Join(templatesPath, templateCommonDir, templateBaseLogin),
-		filepath.Join(templatesPath, templateClientDir, templateResetPassword),
+		filepath.Join(templatesPath, templateCommonDir, templateCommonBaseLogin),
+		filepath.Join(templatesPath, templateCommonDir, templateResetPassword),
 	}
 	viewPDFPaths := []string{
 		filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
@@ -519,7 +471,7 @@ func loadClientTemplates(templatesPath string) {
 	}
 	shareLoginPath := []string{
 		filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
-		filepath.Join(templatesPath, templateCommonDir, templateBaseLogin),
+		filepath.Join(templatesPath, templateCommonDir, templateCommonBaseLogin),
 		filepath.Join(templatesPath, templateClientDir, templateShareLogin),
 	}
 	shareUploadPath := []string{
@@ -536,11 +488,11 @@ func loadClientTemplates(templatesPath string) {
 	filesTmpl := util.LoadTemplate(nil, filesPaths...)
 	profileTmpl := util.LoadTemplate(nil, profilePaths...)
 	changePwdTmpl := util.LoadTemplate(nil, changePwdPaths...)
-	loginTmpl := util.LoadTemplate(nil, loginPath...)
-	messageTmpl := util.LoadTemplate(nil, messagePath...)
-	mfaTmpl := util.LoadTemplate(nil, mfaPath...)
-	twoFactorTmpl := util.LoadTemplate(nil, twoFactorPath...)
-	twoFactorRecoveryTmpl := util.LoadTemplate(nil, twoFactorRecoveryPath...)
+	loginTmpl := util.LoadTemplate(nil, loginPaths...)
+	messageTmpl := util.LoadTemplate(nil, messagePaths...)
+	mfaTmpl := util.LoadTemplate(nil, mfaPaths...)
+	twoFactorTmpl := util.LoadTemplate(nil, twoFactorPaths...)
+	twoFactorRecoveryTmpl := util.LoadTemplate(nil, twoFactorRecoveryPaths...)
 	editFileTmpl := util.LoadTemplate(nil, editFilePath...)
 	shareLoginTmpl := util.LoadTemplate(nil, shareLoginPath...)
 	sharesTmpl := util.LoadTemplate(nil, sharesPaths...)
@@ -554,11 +506,11 @@ func loadClientTemplates(templatesPath string) {
 	clientTemplates[templateClientFiles] = filesTmpl
 	clientTemplates[templateClientProfile] = profileTmpl
 	clientTemplates[templateClientChangePwd] = changePwdTmpl
-	clientTemplates[templateClientLogin] = loginTmpl
+	clientTemplates[templateCommonLogin] = loginTmpl
 	clientTemplates[templateClientMessage] = messageTmpl
 	clientTemplates[templateClientMFA] = mfaTmpl
-	clientTemplates[templateClientTwoFactor] = twoFactorTmpl
-	clientTemplates[templateClientTwoFactorRecovery] = twoFactorRecoveryTmpl
+	clientTemplates[templateTwoFactor] = twoFactorTmpl
+	clientTemplates[templateTwoFactorRecovery] = twoFactorRecoveryTmpl
 	clientTemplates[templateClientEditFile] = editFileTmpl
 	clientTemplates[templateClientShares] = sharesTmpl
 	clientTemplates[templateClientShare] = shareTmpl
@@ -600,7 +552,7 @@ func (s *httpdServer) getBaseClientPageData(title, currentURL string, r *http.Re
 }
 
 func (s *httpdServer) renderClientForgotPwdPage(w http.ResponseWriter, r *http.Request, err *util.I18nError, ip string) {
-	data := clientForgotPwdPage{
+	data := forgotPwdPage{
 		commonBasePage: getCommonBasePage(r),
 		CurrentURL:     webClientForgotPwdPath,
 		Error:          err,
@@ -613,7 +565,7 @@ func (s *httpdServer) renderClientForgotPwdPage(w http.ResponseWriter, r *http.R
 }
 
 func (s *httpdServer) renderClientResetPwdPage(w http.ResponseWriter, r *http.Request, err *util.I18nError, ip string) {
-	data := clientResetPwdPage{
+	data := resetPwdPage{
 		commonBasePage: getCommonBasePage(r),
 		CurrentURL:     webClientResetPwdPath,
 		Error:          err,
@@ -679,7 +631,7 @@ func (s *httpdServer) renderClientNotFoundPage(w http.ResponseWriter, r *http.Re
 }
 
 func (s *httpdServer) renderClientTwoFactorPage(w http.ResponseWriter, r *http.Request, err *util.I18nError, ip string) {
-	data := clientTwoFactorPage{
+	data := twoFactorPage{
 		commonBasePage: getCommonBasePage(r),
 		Title:          pageTwoFactorTitle,
 		CurrentURL:     webClientTwoFactorPath,
@@ -691,11 +643,11 @@ func (s *httpdServer) renderClientTwoFactorPage(w http.ResponseWriter, r *http.R
 	if next := r.URL.Query().Get("next"); strings.HasPrefix(next, webClientFilesPath) {
 		data.CurrentURL += "?next=" + url.QueryEscape(next)
 	}
-	renderClientTemplate(w, templateClientTwoFactor, data)
+	renderClientTemplate(w, templateTwoFactor, data)
 }
 
 func (s *httpdServer) renderClientTwoFactorRecoveryPage(w http.ResponseWriter, r *http.Request, err *util.I18nError, ip string) {
-	data := clientTwoFactorPage{
+	data := twoFactorPage{
 		commonBasePage: getCommonBasePage(r),
 		Title:          pageTwoFactorRecoveryTitle,
 		CurrentURL:     webClientTwoFactorRecoveryPath,
@@ -703,7 +655,7 @@ func (s *httpdServer) renderClientTwoFactorRecoveryPage(w http.ResponseWriter, r
 		CSRFToken:      createCSRFToken(ip),
 		Branding:       s.binding.Branding.WebClient,
 	}
-	renderClientTemplate(w, templateClientTwoFactorRecovery, data)
+	renderClientTemplate(w, templateTwoFactorRecovery, data)
 }
 
 func (s *httpdServer) renderClientMFAPage(w http.ResponseWriter, r *http.Request) {

+ 2 - 0
internal/util/i18n.go

@@ -21,6 +21,7 @@ import (
 
 // localization id for the Web frontend
 const (
+	I18nSetupTitle                     = "title.setup"
 	I18nLoginTitle                     = "title.login"
 	I18nShareLoginTitle                = "title.share_login"
 	I18nFilesTitle                     = "title.files"
@@ -47,6 +48,7 @@ const (
 	I18nError500Title                  = "title.error500"
 	I18nErrorPDFTitle                  = "title.errorPDF"
 	I18nErrorEditorTitle               = "title.error_editor"
+	I18nErrorSetupInstallCode          = "setup.install_code_mismatch"
 	I18nInvalidAuth                    = "general.invalid_auth_request"
 	I18nError429Message                = "general.error429"
 	I18nError400Message                = "general.error400"

+ 7 - 0
static/locales/en/translation.json

@@ -1,5 +1,6 @@
 {
     "title": {
+        "setup": "Initial Setup",
         "login": "Login",
         "share_login": "Share Login",
         "profile": "Profile",
@@ -28,6 +29,12 @@
         "errorPDF": "Unable to show PDF file",
         "error_editor": "Cannot open file editor"
     },
+    "setup": {
+        "desc": "To start using SFTPGo you need to create an administrator user",
+        "submit": "Create admin and Sign in",
+        "install_code_mismatch": "The installation code does not match",
+        "help_text": "SFTPGo needs your help"
+    },
     "login": {
         "username": "Username",
         "password": "Password",

+ 8 - 1
static/locales/it/translation.json

@@ -1,5 +1,6 @@
 {
     "title": {
+        "setup": "Configurazione iniziale",
         "login": "Accedi",
         "share_login": "Accedi alla condivisione",
         "profile": "Profilo",
@@ -28,6 +29,12 @@
         "errorPDF": "Impossibile mostrare il file PDF",
         "error_editor": "Impossibile aprire l'editor di file"
     },
+    "setup": {
+        "desc": "Per iniziare a utilizzare SFTPGo devi creare un utente amministratore",
+        "submit": "Crea amministratore e accedi",
+        "install_code_mismatch": "Il codice di installazione non corrisponde",
+        "help_text": "SFTPGo ha bisogno del tuo aiuto"
+    },
     "login": {
         "username": "Nome utente",
         "password": "Password",
@@ -353,7 +360,7 @@
         "info": "Inserisci la password attuale, per ragioni di sicurezza, e poi la nuova password due volte, per verificare di averla scritta correttamente. Verrai disconnesso dopo aver modificato la tua password",
         "current": "Password attuale",
         "new": "Nuova password",
-        "confirm": "Conferma nuova password",
+        "confirm": "Conferma password",
         "save": "Modifica la mia password",
         "required_fields": "Si prega di fornire la password attuale e quella nuova due volte",
         "no_match": "I due campi della password non corrispondono",

+ 1 - 1
templates/common/base.html

@@ -128,6 +128,7 @@ explicit grant from the SFTPGo Team ([email protected]).
     };
 
     const renderI18n = () => {
+        $('title').text('{{.Branding.Name}} - '+$.t('{{.Title}}'));
         $('body').localize();
         let select2elements = [].slice.call(document.querySelectorAll('[data-control="i18n-select2"]'));
         select2elements.map(function (element){
@@ -209,7 +210,6 @@ explicit grant from the SFTPGo Team ([email protected]).
                     }
 
                     renderI18n();
-                    $('title').text('{{.Branding.Name}} - '+$.t('{{.Title}}'));
                     $.event.trigger({
                         type: "i18nload"
                     });

+ 51 - 96
templates/common/forgot-password.html

@@ -1,104 +1,59 @@
 <!--
-Copyright (C) 2019-2023 Nicola Murino
+Copyright (C) 2023 Nicola Murino
 
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU Affero General Public License as published
-by the Free Software Foundation, version 3.
+This WebUI uses the KeenThemes Mega Bundle, a proprietary theme:
 
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU Affero General Public License for more details.
+https://keenthemes.com/products/templates-mega-bundle
 
-You should have received a copy of the GNU Affero General Public License
-along with this program. If not, see <https://www.gnu.org/licenses/>.
--->
-<!DOCTYPE html>
-<html lang="en">
-
-<head>
-
-    <meta charset="utf-8">
-    <meta http-equiv="X-UA-Compatible" content="IE=edge">
-    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
-    <meta name="description" content="">
-    <meta name="author" content="">
-
-    <title>{{.Branding.Name}} - {{.Title}}</title>
-
-    <link rel="shortcut icon" href="{{.StaticURL}}{{.Branding.FaviconPath}}" />
-
-    <!-- Custom styles for this template-->
-    {{- range .Branding.DefaultCSS}}
-    <link href="{{$.StaticURL}}{{.}}" rel="stylesheet" type="text/css">
-    {{- end}}
-    <style>
-        {{template "commoncss" .}}
-    </style>
-
-    {{range .Branding.ExtraCSS}}
-    <link href="{{$.StaticURL}}{{.}}" rel="stylesheet" type="text/css">
-    {{end}}
-
-</head>
-
-<body class="bg-gradient-primary">
-
-    <div class="container">
-
-        <!-- Outer Row -->
-        <div class="row justify-content-center">
-
-            <div class="col-xl-6 col-lg-7 col-md-9">
+KeenThemes HTML/CSS/JS components are allowed for use only within the
+SFTPGo product and restricted to be used in a resealable HTML template
+that can compete with KeenThemes products anyhow.
 
-                <div class="card o-hidden border-0 shadow-lg my-5">
-                    <div class="card-body p-0">
-                        <!-- Nested Row within Card Body -->
-                        <div class="row">
-                            <div class="col-lg-12">
-                                <div class="p-5">
-                                    <div class="text-center">
-                                        <h1 class="h4 text-gray-900 mb-4">Forgot Your Password?</h1>
-                                        <p class="mb-4">Enter your account username below, you will receive a password reset code by email.</p>
-                                    </div>
-                                    {{if .Error}}
-                                    <div class="alert alert-warning alert-dismissible fade show" role="alert">
-                                        {{.Error}}
-                                        <button type="button" class="close" data-dismiss="alert" aria-label="Close">
-                                            <span aria-hidden="true">&times;</span>
-                                        </button>
-                                    </div>
-                                    {{end}}
-                                    <form id="forgot_password_form" action="{{.CurrentURL}}" method="POST" autocomplete="off"
-                                        class="user-custom">
-                                        <div class="form-group">
-                                            <input type="text" class="form-control form-control-user-custom"
-                                                id="inputUsername" name="username" placeholder="Your username" spellcheck="false" required>
-                                        </div>
-                                        <input type="hidden" name="_form_token" value="{{.CSRFToken}}">
-                                        <button type="submit" class="btn btn-primary btn-user-custom btn-block">
-                                            Send Reset Code
-                                        </button>
-                                    </form>
-                                </div>
-                            </div>
-                        </div>
-                    </div>
-                </div>
+This WebUI is allowed for use only within the SFTPGo product and
+therefore cannot be used in derivative works/products without an
+explicit grant from the SFTPGo Team ([email protected]).
+-->
+{{- template "baselogin" .}}
+
+{{- define "content"}}
+<form class="form w-100" id="sign_in_form" action="{{.CurrentURL}}" method="POST">
+    <div class="container mb-10">
+        <div class="row align-items-center">
+            <div class="col-5 align-items-center">
+                <a href="{{.LoginURL}}">
+                    <img alt="Logo" src="{{.StaticURL}}{{.Branding.LogoPath}}" class="h-80px h-md-90px h-lg-100px" />
+                </a>
+            </div>
+            <div class="col-7">
+                <a href="{{.LoginURL}}" class="text-gray-900 mb-3 ms-3 fs-1 fw-bold">
+                    {{.Branding.ShortName}}
+                </a>
             </div>
         </div>
     </div>
-
-    <!-- Bootstrap core JavaScript-->
-    <script src="{{.StaticURL}}/vendor/jquery/jquery.min.js"></script>
-    <script src="{{.StaticURL}}/vendor/bootstrap/js/bootstrap.bundle.min.js"></script>
-
-    <!-- Core plugin JavaScript-->
-    <script src="{{.StaticURL}}/vendor/jquery-easing/jquery.easing.min.js"></script>
-
-    <!-- Custom scripts for all pages-->
-    <script src="{{.StaticURL}}/js/sb-admin-2.min.js"></script>
-
-</body>
-
-</html>
+    <div class="text-center mb-10">
+        <h2 data-i18n="login.forgot_password" class="text-gray-900 mb-3">
+            Forgot Password ?
+        </h2>
+        <div class="text-gray-700 fw-semibold fs-4">
+            <span data-i18n="login.forgot_password_msg">
+                Enter your account username below, you will receive a password reset code by email.
+            </span>
+        </div>
+    </div>
+    {{- template "errmsg" .Error}}
+    <div class="fv-row mb-10">
+        <input data-i18n="[placeholder]login.your_username" class="form-control form-control-lg form-control-solid" type="text" placeholder="Your username" name="username" spellcheck="false" required />
+    </div>
+    <div class="text-center">
+        <input type="hidden" name="_form_token" value="{{.CSRFToken}}">
+        <button type="submit" id="sign_in_submit" class="btn btn-lg btn-primary w-100 mb-5">
+            <span data-i18n="login.send_reset_code" class="indicator-label">Send Reset Code</span>
+            <span data-i18n="general.wait" class="indicator-progress">
+                Please wait...
+                <span class="spinner-border spinner-border-sm align-middle ms-2"></span>
+            </span>
+        </button>
+    </div>
+</form>
+{{- end}}

+ 0 - 0
templates/webclient/login.html → templates/common/login.html


+ 89 - 100
templates/common/reset-password.html

@@ -1,108 +1,97 @@
 <!--
-Copyright (C) 2019-2023 Nicola Murino
+Copyright (C) 2023 Nicola Murino
 
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU Affero General Public License as published
-by the Free Software Foundation, version 3.
+This WebUI uses the KeenThemes Mega Bundle, a proprietary theme:
 
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU Affero General Public License for more details.
+https://keenthemes.com/products/templates-mega-bundle
 
-You should have received a copy of the GNU Affero General Public License
-along with this program. If not, see <https://www.gnu.org/licenses/>.
--->
-<!DOCTYPE html>
-<html lang="en">
-
-<head>
-
-    <meta charset="utf-8">
-    <meta http-equiv="X-UA-Compatible" content="IE=edge">
-    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
-    <meta name="description" content="">
-    <meta name="author" content="">
-
-    <title>{{.Branding.Name}} - {{.Title}}</title>
-
-    <link rel="shortcut icon" href="{{.StaticURL}}{{.Branding.FaviconPath}}" />
-
-    <!-- Custom styles for this template-->
-    {{- range .Branding.DefaultCSS}}
-    <link href="{{$.StaticURL}}{{.}}" rel="stylesheet" type="text/css">
-    {{- end}}
-    <style>
-        {{template "commoncss" .}}
-    </style>
-
-    {{range .Branding.ExtraCSS}}
-    <link href="{{$.StaticURL}}{{.}}" rel="stylesheet" type="text/css">
-    {{end}}
+KeenThemes HTML/CSS/JS components are allowed for use only within the
+SFTPGo product and restricted to be used in a resealable HTML template
+that can compete with KeenThemes products anyhow.
 
-</head>
-
-<body class="bg-gradient-primary">
-
-    <div class="container">
-
-        <!-- Outer Row -->
-        <div class="row justify-content-center">
-
-            <div class="col-xl-6 col-lg-7 col-md-9">
-
-                <div class="card o-hidden border-0 shadow-lg my-5">
-                    <div class="card-body p-0">
-                        <!-- Nested Row within Card Body -->
-                        <div class="row">
-                            <div class="col-lg-12">
-                                <div class="p-5">
-                                    <div class="text-center">
-                                        <h1 class="h4 text-gray-900 mb-4">Reset Password</h1>
-                                        <p class="mb-4">Check your email for the confirmation code</p>
-                                    </div>
-                                    {{if .Error}}
-                                    <div class="alert alert-warning alert-dismissible fade show" role="alert">
-                                        {{.Error}}
-                                        <button type="button" class="close" data-dismiss="alert" aria-label="Close">
-                                            <span aria-hidden="true">&times;</span>
-                                        </button>
-                                    </div>
-                                    {{end}}
-                                    <form id="forgot_password_form" action="{{.CurrentURL}}" method="POST" autocomplete="off"
-                                        class="user-custom">
-                                        <div class="form-group">
-                                            <input type="text" class="form-control form-control-user-custom"
-                                                id="inputCode" name="code" placeholder="Confirmation code" spellcheck="false" required>
-                                        </div>
-                                        <div class="form-group">
-                                            <input type="password" class="form-control form-control-user-custom"
-                                                id="inputPassword" name="password" placeholder="New Password" autocomplete="new-password" spellcheck="false" required>
-                                        </div>
-                                        <input type="hidden" name="_form_token" value="{{.CSRFToken}}">
-                                        <button type="submit" class="btn btn-primary btn-user-custom btn-block">
-                                            Update Password & Login
-                                        </button>
-                                    </form>
-                                </div>
-                            </div>
-                        </div>
-                    </div>
-                </div>
+This WebUI is allowed for use only within the SFTPGo product and
+therefore cannot be used in derivative works/products without an
+explicit grant from the SFTPGo Team ([email protected]).
+-->
+{{- template "baselogin" .}}
+
+{{- define "content"}}
+<form class="form w-100" id="sign_in_form" action="{{.CurrentURL}}" method="POST">
+    <div class="container mb-10">
+        <div class="row align-items-center">
+            <div class="col-5 align-items-center">
+                <a href="{{.LoginURL}}">
+                    <img alt="Logo" src="{{.StaticURL}}{{.Branding.LogoPath}}" class="h-80px h-md-90px h-lg-100px" />
+                </a>
+            </div>
+            <div class="col-7">
+                <a href="{{.LoginURL}}" class="text-gray-900 mb-3 ms-3 fs-1 fw-bold">
+                    {{.Branding.ShortName}}
+                </a>
             </div>
         </div>
     </div>
-
-    <!-- Bootstrap core JavaScript-->
-    <script src="{{.StaticURL}}/vendor/jquery/jquery.min.js"></script>
-    <script src="{{.StaticURL}}/vendor/bootstrap/js/bootstrap.bundle.min.js"></script>
-
-    <!-- Core plugin JavaScript-->
-    <script src="{{.StaticURL}}/vendor/jquery-easing/jquery.easing.min.js"></script>
-
-    <!-- Custom scripts for all pages-->
-    <script src="{{.StaticURL}}/js/sb-admin-2.min.js"></script>
-
-</body>
-
-</html>
+    <div class="text-center mb-10">
+        <h2 data-i18n="login.reset_password" class="text-gray-900 mb-3">
+            Reset Password
+        </h2>
+        <div class="text-gray-700 fw-semibold fs-4">
+            <span data-i18n="login.reset_pwd_msg">
+                Check your email for the confirmation code
+            </span>
+        </div>
+    </div>
+    {{- template "errmsg" .Error}}
+    <div class="fv-row mb-10">
+        <input data-i18n="[placeholder]login.confirm_code" class="form-control form-control-lg form-control-solid" type="text" placeholder="Confirmation code" name="code" spellcheck="false" required />
+    </div>
+    <div class="fv-row mb-10">
+        <div class="position-relative" data-password-control="container">
+            <input data-i18n="[placeholder]general.new_password" data-password-control="input" class="form-control form-control-lg form-control-solid"
+                type="password" name="password" placeholder="New Password" autocomplete="new-password" spellcheck="false" required />
+            <span class="btn btn-sm btn-icon position-absolute translate-middle top-50 end-0 me-n2" data-password-control="visibility">
+                <i class="ki-duotone ki-eye-slash fs-1">
+                    <span class="path1"></span>
+                    <span class="path2"></span>
+                    <span class="path3"></span>
+                    <span class="path4"></span>
+                </i>
+                <i class="ki-duotone ki-eye d-none fs-1">
+                    <span class="path1"></span>
+                    <span class="path2"></span>
+                    <span class="path3"></span>
+                </i>
+            </span>
+        </div>
+    </div>
+    <div class="fv-row mb-10">
+        <div class="position-relative" data-password-control="container">
+            <input data-i18n="[placeholder]change_pwd.confirm" data-password-control="input" class="form-control form-control-lg form-control-solid"
+                type="password" name="confirm_password" placeholder="Confirm Password" autocomplete="new-password" spellcheck="false" required />
+            <span class="btn btn-sm btn-icon position-absolute translate-middle top-50 end-0 me-n2" data-password-control="visibility">
+                <i class="ki-duotone ki-eye-slash fs-1">
+                    <span class="path1"></span>
+                    <span class="path2"></span>
+                    <span class="path3"></span>
+                    <span class="path4"></span>
+                </i>
+                <i class="ki-duotone ki-eye d-none fs-1">
+                    <span class="path1"></span>
+                    <span class="path2"></span>
+                    <span class="path3"></span>
+                </i>
+            </span>
+        </div>
+    </div>
+    <div class="text-center">
+        <input type="hidden" name="_form_token" value="{{.CSRFToken}}">
+        <button type="submit" id="sign_in_submit" class="btn btn-lg btn-primary w-100 mb-5">
+            <span data-i18n="login.reset_submit" class="indicator-label">Update Password & Login</span>
+            <span data-i18n="general.wait" class="indicator-progress">
+                Please wait...
+                <span class="spinner-border spinner-border-sm align-middle ms-2"></span>
+            </span>
+        </button>
+    </div>
+</form>
+{{- end}}

+ 0 - 0
templates/webclient/twofactor-recovery.html → templates/common/twofactor-recovery.html


+ 0 - 0
templates/webclient/twofactor.html → templates/common/twofactor.html


+ 101 - 111
templates/webadmin/adminsetup.html

@@ -1,119 +1,109 @@
 <!--
-Copyright (C) 2019-2023 Nicola Murino
+Copyright (C) 2023 Nicola Murino
 
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU Affero General Public License as published
-by the Free Software Foundation, version 3.
+This WebUI uses the KeenThemes Mega Bundle, a proprietary theme:
 
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU Affero General Public License for more details.
+https://keenthemes.com/products/templates-mega-bundle
 
-You should have received a copy of the GNU Affero General Public License
-along with this program. If not, see <https://www.gnu.org/licenses/>.
--->
-<!DOCTYPE html>
-<html lang="en">
-
-<head>
-
-    <meta charset="utf-8">
-    <meta http-equiv="X-UA-Compatible" content="IE=edge">
-    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
-    <meta name="description" content="">
-    <meta name="author" content="">
-
-    <title>SFTPGo - Setup</title>
-
-    <link rel="shortcut icon" href="{{.StaticURL}}{{.Branding.FaviconPath}}" />
-
-    <!-- Custom styles for this template-->
-    {{- range .Branding.DefaultCSS}}
-    <link href="{{$.StaticURL}}{{.}}" rel="stylesheet" type="text/css">
-    {{- end}}
-    <style>
-        {{template "commoncss" .}}
-    </style>
-
-</head>
+KeenThemes HTML/CSS/JS components are allowed for use only within the
+SFTPGo product and restricted to be used in a resealable HTML template
+that can compete with KeenThemes products anyhow.
 
-<body class="bg-gradient-primary">
-
-    <div class="container">
-
-        <!-- Outer Row -->
-        <div class="row justify-content-center">
-
-            <div class="col-xl-7 col-lg-8 col-md-10">
-
-                <div class="card o-hidden border-0 shadow-lg my-5">
-                    <div class="card-body p-0">
-                        <!-- Nested Row within Card Body -->
-                        <div class="row">
-                            <div class="col-lg-12">
-                                <div class="p-5">
-                                    <div class="text-center">
-                                        <h1 class="h5 text-gray-900 mb-4">To start using SFTPGo you need to create an admin user</h1>
-                                    </div>
-                                    {{if .Error}}
-                                    <div class="alert alert-warning alert-dismissible fade show" role="alert">
-                                        {{.Error}}
-                                        <button type="button" class="close" data-dismiss="alert" aria-label="Close">
-                                            <span aria-hidden="true">&times;</span>
-                                        </button>
-                                    </div>
-                                    {{end}}
-                                    <form id="login_form" action="{{.CurrentURL}}" method="POST" autocomplete="off"
-                                        class="user-custom">
-                                        {{if .HasInstallationCode}}
-                                        <div class="form-group">
-                                            <input type="text" class="form-control form-control-user-custom" id="inputInstallCode"
-                                                name="install_code" placeholder="{{.InstallationCodeHint}}" value="" required>
-                                        </div>
-                                        {{end}}
-                                        <div class="form-group">
-                                            <input type="text" class="form-control form-control-user-custom" id="inputUsername"
-                                                name="username" placeholder="Username" value="{{.Username}}" spellcheck="false" required>
-                                        </div>
-                                        <div class="form-group">
-                                            <input type="password" class="form-control form-control-user-custom" id="inputPassword"
-                                                name="password" placeholder="Password" autocomplete="new-password" spellcheck="false" required>
-                                        </div>
-                                        <div class="form-group">
-                                            <input type="password" class="form-control form-control-user-custom" id="inputConfirmPassword"
-                                                name="confirm_password" placeholder="Repeat password" autocomplete="new-password" spellcheck="false" required>
-                                        </div>
-                                        <input type="hidden" name="_form_token" value="{{.CSRFToken}}">
-                                        <button type="submit" class="btn btn-primary btn-user-custom btn-block">
-                                            Create admin
-                                        </button>
-                                    </form>
-                                    {{if not .HideSupportLink}}
-                                    <hr>
-                                    <div class="text-center">
-                                        <a class="small" href="https://github.com/drakkan/sftpgo#sponsors" target="_blank">SFTPGo needs your help</a>
-                                    </div>
-                                    {{end}}
-                                </div>
-                            </div>
-                        </div>
-                    </div>
-                </div>
+This WebUI is allowed for use only within the SFTPGo product and
+therefore cannot be used in derivative works/products without an
+explicit grant from the SFTPGo Team ([email protected]).
+-->
+{{- template "baselogin" .}}
+
+{{- define "content"}}
+<form class="form w-100" id="sign_in_form" action="{{.CurrentURL}}" method="POST">
+    <div class="container mb-10">
+        <div class="row align-items-center">
+            <div class="col-5 align-items-center">
+                <img alt="Logo" src="{{.StaticURL}}{{.Branding.LogoPath}}" class="h-80px h-md-90px h-lg-100px" />
+            </div>
+            <div class="col-7">
+                <span class="text-gray-900 mb-3 ms-3 fs-1 fw-bold">
+                    {{.Branding.ShortName}}
+                </span>
             </div>
         </div>
     </div>
-
-    <!-- Bootstrap core JavaScript-->
-    <script src="{{.StaticURL}}/vendor/jquery/jquery.min.js"></script>
-    <script src="{{.StaticURL}}/vendor/bootstrap/js/bootstrap.bundle.min.js"></script>
-
-    <!-- Core plugin JavaScript-->
-    <script src="{{.StaticURL}}/vendor/jquery-easing/jquery.easing.min.js"></script>
-
-    <!-- Custom scripts for all pages-->
-    <script src="{{.StaticURL}}/js/sb-admin-2.min.js"></script>
-
-</body>
-
-</html>
+    <div class="text-center mb-10">
+        <h2 data-i18n="title.setup" class="text-gray-900 mb-3"></h2>
+        <div class="text-gray-700 fw-semibold fs-4">
+            <span data-i18n="setup.desc"></span>
+        </div>
+    </div>
+    {{- template "errmsg" .Error}}
+    {{- if .HasInstallationCode}}
+    <div class="fv-row mb-10">
+        <input class="form-control form-control-lg form-control-solid" type="text" placeholder="{{.InstallationCodeHint}}" name="install_code" spellcheck="false" required />
+    </div>
+    {{- end}}
+    <div class="fv-row mb-10">
+        <input data-i18n="[placeholder]login.username" class="form-control form-control-lg form-control-solid" type="text" name="username" placeholder="Username" autocomplete="on" spellcheck="false" required />
+    </div>
+    <div class="fv-row mb-10">
+        <div class="position-relative" data-password-control="container">
+            <input data-i18n="[placeholder]login.password" data-password-control="input" class="form-control form-control-lg form-control-solid"
+                type="password" name="password" placeholder="Password" autocomplete="password" spellcheck="false" required />
+            <span class="btn btn-sm btn-icon position-absolute translate-middle top-50 end-0 me-n2" data-password-control="visibility">
+                <i class="ki-duotone ki-eye-slash fs-1">
+                    <span class="path1"></span>
+                    <span class="path2"></span>
+                    <span class="path3"></span>
+                    <span class="path4"></span>
+                </i>
+                <i class="ki-duotone ki-eye d-none fs-1">
+                    <span class="path1"></span>
+                    <span class="path2"></span>
+                    <span class="path3"></span>
+                </i>
+            </span>
+        </div>
+    </div>
+    <div class="fv-row mb-10">
+        <div class="position-relative" data-password-control="container">
+            <input data-i18n="[placeholder]change_pwd.confirm" data-password-control="input" class="form-control form-control-lg form-control-solid"
+                type="password" name="confirm_password" placeholder="Confirm Password" autocomplete="new-password" spellcheck="false" required />
+            <span class="btn btn-sm btn-icon position-absolute translate-middle top-50 end-0 me-n2" data-password-control="visibility">
+                <i class="ki-duotone ki-eye-slash fs-1">
+                    <span class="path1"></span>
+                    <span class="path2"></span>
+                    <span class="path3"></span>
+                    <span class="path4"></span>
+                </i>
+                <i class="ki-duotone ki-eye d-none fs-1">
+                    <span class="path1"></span>
+                    <span class="path2"></span>
+                    <span class="path3"></span>
+                </i>
+            </span>
+        </div>
+    </div>
+    <div class="text-center">
+        <input type="hidden" name="_form_token" value="{{.CSRFToken}}">
+        <button type="submit" id="sign_in_submit" class="btn btn-lg btn-primary w-100 mb-5">
+            <span data-i18n="setup.submit" class="indicator-label"></span>
+            <span data-i18n="general.wait" class="indicator-progress">
+                Please wait...
+                <span class="spinner-border spinner-border-sm align-middle ms-2"></span>
+            </span>
+        </button>
+    </div>
+</form>
+<hr>
+<div class="d-flex flex-stack pt-5 mt-3">
+    <div class="me-10">
+        <select id="languageSwitcher" name="language" class="form-select form-select-solid form-select-sm" data-control="i18n-select2" data-hide-search="true"></select>
+    </div>
+    {{- if not .HideSupportLink}}
+    <div class="d-flex fw-semibold text-primary">
+        <a href="https://github.com/drakkan/sftpgo#sponsors" target="_blank" class="px-2">
+            <span data-i18n="setup.help_text"></span>
+        </a>
+    </div>
+    {{- end}}
+</div>
+{{- end}}

+ 0 - 90
templates/webadmin/baselogin.html

@@ -1,90 +0,0 @@
-<!--
-Copyright (C) 2019-2023 Nicola Murino
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU Affero General Public License as published
-by the Free Software Foundation, version 3.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU Affero General Public License for more details.
-
-You should have received a copy of the GNU Affero General Public License
-along with this program. If not, see <https://www.gnu.org/licenses/>.
--->
-{{define "baselogin"}}
-<!DOCTYPE html>
-<html lang="en">
-
-<head>
-
-    <meta charset="utf-8">
-    <meta http-equiv="X-UA-Compatible" content="IE=edge">
-    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
-    <meta name="description" content="">
-    <meta name="author" content="">
-
-    <title>{{.Branding.Name}} - {{template "title" .}}</title>
-
-    <link rel="shortcut icon" href="{{.StaticURL}}{{.Branding.FaviconPath}}" />
-
-    <!-- Custom styles for this template-->
-    {{- range .Branding.DefaultCSS}}
-    <link href="{{$.StaticURL}}{{.}}" rel="stylesheet" type="text/css">
-    {{- end}}
-    <style>
-        {{template "commoncss" .}}
-    </style>
-
-    {{range .Branding.ExtraCSS}}
-    <link href="{{$.StaticURL}}{{.}}" rel="stylesheet" type="text/css">
-    {{end}}
-
-</head>
-
-<body class="bg-gradient-primary">
-
-    <div class="container">
-
-        <!-- Outer Row -->
-        <div class="row justify-content-center">
-
-            <div class="col-xl-10 col-lg-12 col-md-9">
-
-                <div class="card o-hidden border-0 shadow-lg my-5">
-                    <div class="card-body p-0">
-                        <!-- Nested Row within Card Body -->
-                        <div class="row d-lg-none login-image">
-                            <div class="col-lg-12 d-block d-lg-none bg-login-image">
-                            </div>
-                        </div>
-                        <div class="row">
-                            <div class="col-lg-5 d-none d-lg-block bg-login-image">
-                            </div>
-                            <div class="col-lg-7">
-                                <div class="p-5">
-                                    {{template "content" .}}
-                                </div>
-                            </div>
-                        </div>
-                    </div>
-                </div>
-            </div>
-        </div>
-    </div>
-
-    <!-- Bootstrap core JavaScript-->
-    <script src="{{.StaticURL}}/vendor/jquery/jquery.min.js"></script>
-    <script src="{{.StaticURL}}/vendor/bootstrap/js/bootstrap.bundle.min.js"></script>
-
-    <!-- Core plugin JavaScript-->
-    <script src="{{.StaticURL}}/vendor/jquery-easing/jquery.easing.min.js"></script>
-
-    <!-- Custom scripts for all pages-->
-    <script src="{{.StaticURL}}/js/sb-admin-2.min.js"></script>
-
-</body>
-
-</html>
-{{end}}

+ 92 - 65
templates/webadmin/login.html

@@ -1,72 +1,99 @@
 <!--
-Copyright (C) 2019-2023 Nicola Murino
+Copyright (C) 2023 Nicola Murino
 
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU Affero General Public License as published
-by the Free Software Foundation, version 3.
+This WebUI uses the KeenThemes Mega Bundle, a proprietary theme:
 
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU Affero General Public License for more details.
+https://keenthemes.com/products/templates-mega-bundle
 
-You should have received a copy of the GNU Affero General Public License
-along with this program. If not, see <https://www.gnu.org/licenses/>.
--->
-{{template "baselogin" .}}
+KeenThemes HTML/CSS/JS components are allowed for use only within the
+SFTPGo product and restricted to be used in a resealable HTML template
+that can compete with KeenThemes products anyhow.
 
-{{define "title"}}Login{{end}}
+This WebUI is allowed for use only within the SFTPGo product and
+therefore cannot be used in derivative works/products without an
+explicit grant from the SFTPGo Team ([email protected]).
+-->
+{{- template "baselogin" .}}
 
-{{define "content"}}
-                                    <div class="text-center">
-                                        <h1 class="h4 text-gray-900 mb-4">{{.Branding.ShortName}}</h1>
-                                    </div>
-                                    {{if .Error}}
-                                    <div class="alert alert-warning alert-dismissible fade show" role="alert">
-                                        {{.Error}}
-                                        <button type="button" class="close" data-dismiss="alert" aria-label="Close">
-                                            <span aria-hidden="true">&times;</span>
-                                        </button>
-                                    </div>
-                                    {{end}}
-                                    <form id="login_form" action="{{.CurrentURL}}" method="POST"
-                                        class="user-custom">
-                                        {{if not .FormDisabled}}
-                                        <div class="form-group">
-                                            <input type="text" class="form-control form-control-user-custom"
-                                                id="inputUsername" name="username" placeholder="Username" spellcheck="false" required>
-                                        </div>
-                                        <div class="form-group">
-                                            <input type="password" class="form-control form-control-user-custom"
-                                                id="inputPassword" name="password" placeholder="Password" autocomplete="current-password" spellcheck="false" required>
-                                            {{if .ForgotPwdURL}}
-                                            <div class="text-right">
-                                                <a class="small" href="{{.ForgotPwdURL}}">Forgot password?</a>
-                                            </div>
-                                            {{end}}
-                                        </div>
-                                        <input type="hidden" name="_form_token" value="{{.CSRFToken}}">
-                                        <button type="submit" class="btn btn-primary btn-user-custom btn-block">
-                                            Login
-                                        </button>
-                                        {{end}}
-                                        {{if .OpenIDLoginURL}}
-                                        <hr>
-                                        <a href="{{.OpenIDLoginURL}}" class="btn btn-secondary btn-user-custom btn-block">
-                                            Login with OpenID
-                                        </a>
-                                        {{end}}
-                                    </form>
-                                    {{if .AltLoginURL}}
-                                    <hr>
-                                    <div class="text-center">
-                                        <a class="small" href="{{.AltLoginURL}}">{{.AltLoginName}}</a>
-                                    </div>
-                                    {{end}}
-                                    {{if and .Branding.DisclaimerName .Branding.DisclaimerPath}}
-                                    <hr>
-                                    <div class="text-center">
-                                        <a class="small" href="{{.Branding.DisclaimerPath}}" target="_blank">{{.Branding.DisclaimerName}}</a>
-                                    </div>
-                                    {{end}}
+{{- define "content"}}
+                        <form class="form w-100" id="sign_in_form" action="{{.CurrentURL}}" method="POST">
+							<div class="container mb-10">
+								<div class="row align-items-center">
+									<div class="col-5 align-items-center">
+										<img alt="Logo" src="{{.StaticURL}}{{.Branding.LogoPath}}" class="h-80px h-md-90px h-lg-100px" />
+									</div>
+									<div class="col-7">
+										<h1 class="text-gray-900 mb-3 ms-3">
+											{{.Branding.ShortName}}
+										</h1>
+									</div>
+								</div>
+							</div>
+							{{- template "errmsg" .Error}}
+							{{- if not .FormDisabled}}
+							<div class="fv-row mb-10">
+								<input data-i18n="[placeholder]login.username" class="form-control form-control-lg form-control-solid" type="text" name="username" placeholder="Username" autocomplete="on" spellcheck="false" required />
+							</div>
+							<div class="fv-row mb-10">
+								<div class="position-relative" data-password-control="container">
+									<input data-i18n="[placeholder]login.password" data-password-control="input" class="form-control form-control-lg form-control-solid" type="password" name="password" placeholder="Password" autocomplete="current-password" spellcheck="false" required />
+									<span class="btn btn-sm btn-icon position-absolute translate-middle top-50 end-0 me-n2" data-password-control="visibility">
+                    					<i class="ki-duotone ki-eye-slash fs-1">
+											<span class="path1"></span>
+											<span class="path2"></span>
+											<span class="path3"></span>
+											<span class="path4"></span>
+										</i>
+                    					<i class="ki-duotone ki-eye d-none fs-1">
+											<span class="path1"></span>
+											<span class="path2"></span>
+											<span class="path3"></span>
+										</i>
+									</span>
+								</div>
+								<div class="d-flex justify-content-end mt-2">
+									{{- if .ForgotPwdURL}}
+									<a data-i18n="login.forgot_password" href="{{.ForgotPwdURL}}" class="link-primary fs-6 fw-bold">Forgot Password ?</a>
+									{{- end}}
+								</div>
+							</div>
+							{{- end}}
+							<div class="text-center">
+								{{- if not .FormDisabled}}
+								<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
+								<button type="submit" id="sign_in_submit" class="btn btn-lg btn-primary w-100 mb-5">
+									<span data-i18n="login.signin" class="indicator-label">Sign in</span>
+									<span data-i18n="general.wait" class="indicator-progress">
+										Please wait...
+										<span class="spinner-border spinner-border-sm align-middle ms-2"></span>
+									</span>
+								</button>
+								{{- end}}
+								{{- if .OpenIDLoginURL}}
+								<a href="{{.OpenIDLoginURL}}" class="btn btn-flex btn-outline flex-center {{if .FormDisabled}}btn-primary{{else}}btn-active-color-primary bg-state-light{{end}} btn-lg w-100 my-5">
+									<img alt="Logo" src="{{.StaticURL}}/img/openid-logo.png" class="h-20px me-3" />
+									<span data-i18n="login.signin_openid">Sign in with OpenID</span>
+								</a>
+								{{- end}}
+							</div>
+						</form>
+						<hr>
+						<div class="d-flex flex-stack pt-5 mt-3">
+							<div class="me-10">
+								<select id="languageSwitcher" name="language" class="form-select form-select-solid form-select-sm" data-control="i18n-select2" data-hide-search="true">
+								</select>
+							</div>
+							<div class="d-flex fw-semibold text-primary">
+								{{- if .AltLoginURL}}
+								<a href="{{.AltLoginURL}}" class="px-2">
+									<span data-i18n="login.link" data-i18n-options='{ "link": "{{.AltLoginName}}" }'></span>
+								</a>
+								{{- end}}
+								{{- if and .Branding.DisclaimerName .Branding.DisclaimerPath}}
+								<a href="{{.Branding.DisclaimerPath}}" target="_blank" class="px-2">
+									<span data-i18n="custom.disclaimer_webclient">{{.Branding.DisclaimerName}}</span>
+								</a>
+								{{- end}}
+							</div>
+						</div>
 {{end}}

+ 0 - 47
templates/webadmin/twofactor-recovery.html

@@ -1,47 +0,0 @@
-<!--
-Copyright (C) 2019-2023 Nicola Murino
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU Affero General Public License as published
-by the Free Software Foundation, version 3.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU Affero General Public License for more details.
-
-You should have received a copy of the GNU Affero General Public License
-along with this program. If not, see <https://www.gnu.org/licenses/>.
--->
-{{template "baselogin" .}}
-
-{{define "title"}}{{.Title}}{{end}}
-
-{{define "content"}}
-                                    <div class="text-center">
-                                        <h1 class="h4 text-gray-900 mb-4">{{.Branding.Name}}</h1>
-                                    </div>
-                                    {{if .Error}}
-                                    <div class="alert alert-warning alert-dismissible fade show" role="alert">
-                                        {{.Error}}
-                                        <button type="button" class="close" data-dismiss="alert" aria-label="Close">
-                                            <span aria-hidden="true">&times;</span>
-                                        </button>
-                                    </div>
-                                    {{end}}
-                                    <form id="login_form" action="{{.CurrentURL}}" method="POST" autocomplete="off"
-                                        class="user-custom">
-                                        <div class="form-group">
-                                            <input type="text" class="form-control form-control-user-custom"
-                                                id="inputRecoveryCode" name="recovery_code" placeholder="Recovery code" spellcheck="false" required>
-                                        </div>
-                                        <input type="hidden" name="_form_token" value="{{.CSRFToken}}">
-                                        <button type="submit" class="btn btn-primary btn-user-custom btn-block">
-                                            Verify
-                                        </button>
-                                    </form>
-                                    <hr>
-                                    <div>
-                                        <p>You can enter one of your recovery codes in case you lost access to your mobile device.</p>
-                                    </div>
-{{end}}

+ 0 - 52
templates/webadmin/twofactor.html

@@ -1,52 +0,0 @@
-<!--
-Copyright (C) 2019-2023 Nicola Murino
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU Affero General Public License as published
-by the Free Software Foundation, version 3.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU Affero General Public License for more details.
-
-You should have received a copy of the GNU Affero General Public License
-along with this program. If not, see <https://www.gnu.org/licenses/>.
--->
-{{template "baselogin" .}}
-
-{{define "title"}}{{.Title}}{{end}}
-
-{{define "content"}}
-                                    <div class="text-center">
-                                        <h1 class="h4 text-gray-900 mb-4">{{.Branding.Name}}</h1>
-                                    </div>
-                                    {{if .Error}}
-                                    <div class="alert alert-warning alert-dismissible fade show" role="alert">
-                                        {{.Error}}
-                                        <button type="button" class="close" data-dismiss="alert" aria-label="Close">
-                                            <span aria-hidden="true">&times;</span>
-                                        </button>
-                                    </div>
-                                    {{end}}
-                                    <form id="login_form" action="{{.CurrentURL}}" method="POST" autocomplete="off"
-                                        class="user-custom">
-                                        <div class="form-group">
-                                            <input type="text" class="form-control form-control-user-custom"
-                                                id="inputPasscode" name="passcode" placeholder="Authentication code" spellcheck="false" required>
-                                        </div>
-                                        <input type="hidden" name="_form_token" value="{{.CSRFToken}}">
-                                        <button type="submit" class="btn btn-primary btn-user-custom btn-block">
-                                            Verify
-                                        </button>
-                                    </form>
-                                    <hr>
-                                    <div>
-                                        <p>Open the two-factor authentication app on your device to view your authentication code and verify your identity.</p>
-                                    </div>
-                                    <hr>
-                                    <div>
-                                        <p><strong>Having problems?</strong></p>
-                                        <p><a href="{{.RecoveryURL}}">Enter a two-factor recovery code</a></p>
-                                    </div>
-{{end}}

+ 0 - 59
templates/webclient/forgot-password.html

@@ -1,59 +0,0 @@
-<!--
-Copyright (C) 2023 Nicola Murino
-
-This WebUI uses the KeenThemes Mega Bundle, a proprietary theme:
-
-https://keenthemes.com/products/templates-mega-bundle
-
-KeenThemes HTML/CSS/JS components are allowed for use only within the
-SFTPGo product and restricted to be used in a resealable HTML template
-that can compete with KeenThemes products anyhow.
-
-This WebUI is allowed for use only within the SFTPGo product and
-therefore cannot be used in derivative works/products without an
-explicit grant from the SFTPGo Team ([email protected]).
--->
-{{- template "baselogin" .}}
-
-{{- define "content"}}
-<form class="form w-100" id="sign_in_form" action="{{.CurrentURL}}" method="POST">
-    <div class="container mb-10">
-        <div class="row align-items-center">
-            <div class="col-5 align-items-center">
-                <a href="{{.LoginURL}}">
-                    <img alt="Logo" src="{{.StaticURL}}{{.Branding.LogoPath}}" class="h-80px h-md-90px h-lg-100px" />
-                </a>
-            </div>
-            <div class="col-7">
-                <a href="{{.LoginURL}}" class="text-gray-900 mb-3 ms-3 fs-1 fw-bold">
-                    {{.Branding.ShortName}}
-                </a>
-            </div>
-        </div>
-    </div>
-    <div class="text-center mb-10">
-        <h2 data-i18n="login.forgot_password" class="text-gray-900 mb-3">
-            Forgot Password ?
-        </h2>
-        <div class="text-gray-600 fw-semibold fs-4">
-            <span data-i18n="login.forgot_password_msg">
-                Enter your account username below, you will receive a password reset code by email.
-            </span>
-        </div>
-    </div>
-    {{- template "errmsg" .Error}}
-    <div class="fv-row mb-10">
-        <input data-i18n="[placeholder]login.your_username" class="form-control form-control-lg form-control-solid" type="text" placeholder="Your username" name="username" spellcheck="false" required />
-    </div>
-    <div class="text-center">
-        <input type="hidden" name="_form_token" value="{{.CSRFToken}}">
-        <button type="submit" id="sign_in_submit" class="btn btn-lg btn-primary w-100 mb-5">
-            <span data-i18n="login.send_reset_code" class="indicator-label">Send Reset Code</span>
-            <span data-i18n="general.wait" class="indicator-progress">
-                Please wait...
-                <span class="spinner-border spinner-border-sm align-middle ms-2"></span>
-            </span>
-        </button>
-    </div>
-</form>
-{{- end}}

+ 0 - 97
templates/webclient/reset-password.html

@@ -1,97 +0,0 @@
-<!--
-Copyright (C) 2023 Nicola Murino
-
-This WebUI uses the KeenThemes Mega Bundle, a proprietary theme:
-
-https://keenthemes.com/products/templates-mega-bundle
-
-KeenThemes HTML/CSS/JS components are allowed for use only within the
-SFTPGo product and restricted to be used in a resealable HTML template
-that can compete with KeenThemes products anyhow.
-
-This WebUI is allowed for use only within the SFTPGo product and
-therefore cannot be used in derivative works/products without an
-explicit grant from the SFTPGo Team ([email protected]).
--->
-{{- template "baselogin" .}}
-
-{{- define "content"}}
-<form class="form w-100" id="sign_in_form" action="{{.CurrentURL}}" method="POST">
-    <div class="container mb-10">
-        <div class="row align-items-center">
-            <div class="col-5 align-items-center">
-                <a href="{{.LoginURL}}">
-                    <img alt="Logo" src="{{.StaticURL}}{{.Branding.LogoPath}}" class="h-80px h-md-90px h-lg-100px" />
-                </a>
-            </div>
-            <div class="col-7">
-                <a href="{{.LoginURL}}" class="text-gray-900 mb-3 ms-3 fs-1 fw-bold">
-                    {{.Branding.ShortName}}
-                </a>
-            </div>
-        </div>
-    </div>
-    <div class="text-center mb-10">
-        <h2 data-i18n="login.reset_password" class="text-gray-900 mb-3">
-            Reset Password
-        </h2>
-        <div class="text-gray-600 fw-semibold fs-4">
-            <span data-i18n="login.reset_pwd_msg">
-                Check your email for the confirmation code
-            </span>
-        </div>
-    </div>
-    {{- template "errmsg" .Error}}
-    <div class="fv-row mb-10">
-        <input data-i18n="[placeholder]login.confirm_code" class="form-control form-control-lg form-control-solid" type="text" placeholder="Confirmation code" name="code" spellcheck="false" required />
-    </div>
-    <div class="fv-row mb-10">
-        <div class="position-relative" data-password-control="container">
-            <input data-i18n="[placeholder]general.new_password" data-password-control="input" class="form-control form-control-lg form-control-solid"
-                type="password" name="password" placeholder="New Password" autocomplete="new-password" spellcheck="false" required />
-            <span class="btn btn-sm btn-icon position-absolute translate-middle top-50 end-0 me-n2" data-password-control="visibility">
-                <i class="ki-duotone ki-eye-slash fs-1">
-                    <span class="path1"></span>
-                    <span class="path2"></span>
-                    <span class="path3"></span>
-                    <span class="path4"></span>
-                </i>
-                <i class="ki-duotone ki-eye d-none fs-1">
-                    <span class="path1"></span>
-                    <span class="path2"></span>
-                    <span class="path3"></span>
-                </i>
-            </span>
-        </div>
-    </div>
-    <div class="fv-row mb-10">
-        <div class="position-relative" data-password-control="container">
-            <input data-i18n="[placeholder]general.confirm_password" data-password-control="input" class="form-control form-control-lg form-control-solid"
-                type="password" name="confirm_password" placeholder="Confirm Password" autocomplete="new-password" spellcheck="false" required />
-            <span class="btn btn-sm btn-icon position-absolute translate-middle top-50 end-0 me-n2" data-password-control="visibility">
-                <i class="ki-duotone ki-eye-slash fs-1">
-                    <span class="path1"></span>
-                    <span class="path2"></span>
-                    <span class="path3"></span>
-                    <span class="path4"></span>
-                </i>
-                <i class="ki-duotone ki-eye d-none fs-1">
-                    <span class="path1"></span>
-                    <span class="path2"></span>
-                    <span class="path3"></span>
-                </i>
-            </span>
-        </div>
-    </div>
-    <div class="text-center">
-        <input type="hidden" name="_form_token" value="{{.CSRFToken}}">
-        <button type="submit" id="sign_in_submit" class="btn btn-lg btn-primary w-100 mb-5">
-            <span data-i18n="login.reset_submit" class="indicator-label">Update Password & Login</span>
-            <span data-i18n="general.wait" class="indicator-progress">
-                Please wait...
-                <span class="spinner-border spinner-border-sm align-middle ms-2"></span>
-            </span>
-        </button>
-    </div>
-</form>
-{{- end}}