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

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2023-12-30 19:12:22 +01:00
parent 7318d1f32a
commit 3e47a4f664
No known key found for this signature in database
GPG key ID: 935D2952DEC4EECF
25 changed files with 514 additions and 919 deletions

View file

@ -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:support@sftpgo.com).
- 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:support@sftpgo.com).

View file

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

View file

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

View file

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

View file

@ -441,14 +441,10 @@ 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"}
}
}
for idx := range b.ExtraCSS {
b.ExtraCSS[idx] = util.CleanPath(b.ExtraCSS[idx])

View file

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

View file

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

View file

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

View file

@ -47,13 +47,10 @@ 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"
@ -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) {

View file

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

View file

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

View file

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

View file

@ -128,6 +128,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
};
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 (support@sftpgo.com).
}
renderI18n();
$('title').text('{{.Branding.Name}} - '+$.t('{{.Title}}'));
$.event.trigger({
type: "i18nload"
});

View file

@ -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/>.
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 (support@sftpgo.com).
-->
<!DOCTYPE html>
<html lang="en">
{{- template "baselogin" .}}
<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">
<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">
{{- 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-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">
<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 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>
</form>
</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>
</form>
{{- end}}

View file

@ -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/>.
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 (support@sftpgo.com).
-->
<!DOCTYPE html>
<html lang="en">
{{- template "baselogin" .}}
<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">
<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">
{{- 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-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">
<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 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>
</form>
</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>
</form>
{{- end}}

View file

@ -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/>.
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 (support@sftpgo.com).
-->
<!DOCTYPE html>
<html lang="en">
{{- template "baselogin" .}}
<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">
{{- 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>
<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}}
<style>
{{template "commoncss" .}}
</style>
</head>
<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="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">
<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 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>
</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}}
</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>
</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}}
</div>
{{- end}}

View file

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

View file

@ -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/>.
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 (support@sftpgo.com).
-->
{{template "baselogin" .}}
{{- template "baselogin" .}}
{{define "title"}}Login{{end}}
{{define "content"}}
{{- 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">
<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>
{{- if not .FormDisabled}}
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<button type="submit" class="btn btn-primary btn-user-custom btn-block">
Login
<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}}
<hr>
<a href="{{.OpenIDLoginURL}}" class="btn btn-secondary btn-user-custom btn-block">
Login with OpenID
{{- 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}}
{{- end}}
</div>
</form>
{{if .AltLoginURL}}
<hr>
<div class="text-center">
<a class="small" href="{{.AltLoginURL}}">{{.AltLoginName}}</a>
<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>
{{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}}
{{end}}

View file

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

View file

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

View file

@ -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 (support@sftpgo.com).
-->
{{- 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}}

View file

@ -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 (support@sftpgo.com).
-->
{{- 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}}