Browse Source

web admin: make base url configurable

Nicola Murino 4 years ago
parent
commit
0bc4db9950

+ 4 - 0
config/config.go

@@ -218,8 +218,11 @@ func Init() {
 			TemplatesPath:      "templates",
 			StaticFilesPath:    "static",
 			BackupsPath:        "backups",
+			WebAdminRoot:       "",
 			CertificateFile:    "",
 			CertificateKeyFile: "",
+			CACertificates:     nil,
+			CARevocationLists:  nil,
 		},
 		HTTPConfig: httpclient.Config{
 			Timeout:        20,
@@ -857,6 +860,7 @@ func setViperDefaults() {
 	viper.SetDefault("httpd.templates_path", globalConf.HTTPDConfig.TemplatesPath)
 	viper.SetDefault("httpd.static_files_path", globalConf.HTTPDConfig.StaticFilesPath)
 	viper.SetDefault("httpd.backups_path", globalConf.HTTPDConfig.BackupsPath)
+	viper.SetDefault("httpd.web_admin_root", globalConf.HTTPDConfig.WebAdminRoot)
 	viper.SetDefault("httpd.certificate_file", globalConf.HTTPDConfig.CertificateFile)
 	viper.SetDefault("httpd.certificate_key_file", globalConf.HTTPDConfig.CertificateKeyFile)
 	viper.SetDefault("httpd.ca_certificates", globalConf.HTTPDConfig.CACertificates)

+ 1 - 0
docs/full-configuration.md

@@ -204,6 +204,7 @@ The configuration file contains the following sections:
   - `templates_path`, string. Path to the HTML web templates. This can be an absolute path or a path relative to the config dir
   - `static_files_path`, string. Path to the static files for the web interface. This can be an absolute path or a path relative to the config dir. If both `templates_path` and `static_files_path` are empty the built-in web interface will be disabled
   - `backups_path`, string. Path to the backup directory. This can be an absolute path or a path relative to the config dir. We don't allow backups in arbitrary paths for security reasons
+  - `web_admin_root`, string.  Defines a base URL for the web admin. If empty web admin resources will be available at the root ("/") URI. If defined it must be an absolute URI or it will be ignored
   - `certificate_file`, string. Certificate for HTTPS. This can be an absolute path or a path relative to the config dir.
   - `certificate_key_file`, string. Private key matching the above certificate. This can be an absolute path or a path relative to the config dir. If both the certificate and the private key are provided, the server will expect HTTPS connections. Certificate and key files can be reloaded on demand sending a `SIGHUP` signal on Unix based systems and a `paramchange` request to the running service on Windows.
   - `ca_certificates`, list of strings. Set of root certificate authorities to be used to verify client certificates.

+ 98 - 40
httpd/httpd.go

@@ -8,6 +8,7 @@ import (
 	"fmt"
 	"net/http"
 	"net/url"
+	"path"
 	"path/filepath"
 	"runtime"
 	"strings"
@@ -27,46 +28,47 @@ import (
 )
 
 const (
-	logSender                 = "httpd"
-	tokenPath                 = "/api/v2/token"
-	logoutPath                = "/api/v2/logout"
-	activeConnectionsPath     = "/api/v2/connections"
-	quotaScanPath             = "/api/v2/quota-scans"
-	quotaScanVFolderPath      = "/api/v2/folder-quota-scans"
-	userPath                  = "/api/v2/users"
-	versionPath               = "/api/v2/version"
-	folderPath                = "/api/v2/folders"
-	serverStatusPath          = "/api/v2/status"
-	dumpDataPath              = "/api/v2/dumpdata"
-	loadDataPath              = "/api/v2/loaddata"
-	updateUsedQuotaPath       = "/api/v2/quota-update"
-	updateFolderUsedQuotaPath = "/api/v2/folder-quota-update"
-	defenderBanTime           = "/api/v2/defender/bantime"
-	defenderUnban             = "/api/v2/defender/unban"
-	defenderScore             = "/api/v2/defender/score"
-	adminPath                 = "/api/v2/admins"
-	adminPwdPath              = "/api/v2/changepwd/admin"
-	healthzPath               = "/healthz"
-	webBasePath               = "/web"
-	webLoginPath              = "/web/login"
-	webLogoutPath             = "/web/logout"
-	webUsersPath              = "/web/users"
-	webUserPath               = "/web/user"
-	webConnectionsPath        = "/web/connections"
-	webFoldersPath            = "/web/folders"
-	webFolderPath             = "/web/folder"
-	webStatusPath             = "/web/status"
-	webAdminsPath             = "/web/admins"
-	webAdminPath              = "/web/admin"
-	webMaintenancePath        = "/web/maintenance"
-	webBackupPath             = "/web/backup"
-	webRestorePath            = "/web/restore"
-	webScanVFolderPath        = "/web/folder-quota-scans"
-	webQuotaScanPath          = "/web/quota-scans"
-	webChangeAdminPwdPath     = "/web/changepwd/admin"
-	webTemplateUser           = "/web/template/user"
-	webTemplateFolder         = "/web/template/folder"
-	webStaticFilesPath        = "/static"
+	logSender                    = "httpd"
+	tokenPath                    = "/api/v2/token"
+	logoutPath                   = "/api/v2/logout"
+	activeConnectionsPath        = "/api/v2/connections"
+	quotaScanPath                = "/api/v2/quota-scans"
+	quotaScanVFolderPath         = "/api/v2/folder-quota-scans"
+	userPath                     = "/api/v2/users"
+	versionPath                  = "/api/v2/version"
+	folderPath                   = "/api/v2/folders"
+	serverStatusPath             = "/api/v2/status"
+	dumpDataPath                 = "/api/v2/dumpdata"
+	loadDataPath                 = "/api/v2/loaddata"
+	updateUsedQuotaPath          = "/api/v2/quota-update"
+	updateFolderUsedQuotaPath    = "/api/v2/folder-quota-update"
+	defenderBanTime              = "/api/v2/defender/bantime"
+	defenderUnban                = "/api/v2/defender/unban"
+	defenderScore                = "/api/v2/defender/score"
+	adminPath                    = "/api/v2/admins"
+	adminPwdPath                 = "/api/v2/changepwd/admin"
+	healthzPath                  = "/healthz"
+	webRootPathDefault           = "/"
+	webBasePathDefault           = "/web"
+	webLoginPathDefault          = "/web/login"
+	webLogoutPathDefault         = "/web/logout"
+	webUsersPathDefault          = "/web/users"
+	webUserPathDefault           = "/web/user"
+	webConnectionsPathDefault    = "/web/connections"
+	webFoldersPathDefault        = "/web/folders"
+	webFolderPathDefault         = "/web/folder"
+	webStatusPathDefault         = "/web/status"
+	webAdminsPathDefault         = "/web/admins"
+	webAdminPathDefault          = "/web/admin"
+	webMaintenancePathDefault    = "/web/maintenance"
+	webBackupPathDefault         = "/web/backup"
+	webRestorePathDefault        = "/web/restore"
+	webScanVFolderPathDefault    = "/web/folder-quota-scans"
+	webQuotaScanPathDefault      = "/web/quota-scans"
+	webChangeAdminPwdPathDefault = "/web/changepwd/admin"
+	webTemplateUserDefault       = "/web/template/user"
+	webTemplateFolderDefault     = "/web/template/folder"
+	webStaticFilesPathDefault    = "/static"
 	// MaxRestoreSize defines the max size for the loaddata input file
 	MaxRestoreSize = 10485760 // 10 MB
 	maxRequestSize = 1048576  // 1MB
@@ -80,8 +82,33 @@ var (
 	jwtTokensCleanupDone   chan bool
 	invalidatedJWTTokens   sync.Map
 	csrfTokenAuth          *jwtauth.JWTAuth
+	webRootPath            string
+	webBasePath            string
+	webLoginPath           string
+	webLogoutPath          string
+	webUsersPath           string
+	webUserPath            string
+	webConnectionsPath     string
+	webFoldersPath         string
+	webFolderPath          string
+	webStatusPath          string
+	webAdminsPath          string
+	webAdminPath           string
+	webMaintenancePath     string
+	webBackupPath          string
+	webRestorePath         string
+	webScanVFolderPath     string
+	webQuotaScanPath       string
+	webChangeAdminPwdPath  string
+	webTemplateUser        string
+	webTemplateFolder      string
+	webStaticFilesPath     string
 )
 
+func init() {
+	updateWebAdminURLs("")
+}
+
 // Binding defines the configuration for a network listener
 type Binding struct {
 	// The address to listen on. A blank value means listen on all available network interfaces.
@@ -153,6 +180,9 @@ type Conf struct {
 	StaticFilesPath string `json:"static_files_path" mapstructure:"static_files_path"`
 	// Path to the backup directory. This can be an absolute path or a path relative to the config dir
 	BackupsPath string `json:"backups_path" mapstructure:"backups_path"`
+	// Defines a base URL for the web admin. If empty web admin resources will be available at the
+	// root ("/") URI. If defined it must be an absolute URI or it will be ignored.
+	WebAdminRoot string `json:"web_admin_root" mapstructure:"web_admin_root"`
 	// If files containing a certificate and matching private key for the server are provided the server will expect
 	// HTTPS connections.
 	// Certificate and key files can be reloaded on demand sending a "SIGHUP" signal on Unix based systems and a
@@ -199,6 +229,7 @@ func (c *Conf) Initialize(configDir string) error {
 	certificateFile := getConfigPath(c.CertificateFile, configDir)
 	certificateKeyFile := getConfigPath(c.CertificateKeyFile, configDir)
 	if enableWebAdmin {
+		updateWebAdminURLs(c.WebAdminRoot)
 		loadTemplates(templatesPath)
 	} else {
 		logger.Info(logSender, "", "built-in web interface disabled, please set templates_path and static_files_path to enable it")
@@ -298,6 +329,33 @@ func fileServer(r chi.Router, path string, root http.FileSystem) {
 	})
 }
 
+func updateWebAdminURLs(baseURL string) {
+	if !path.IsAbs(baseURL) {
+		baseURL = "/"
+	}
+	webRootPath = path.Join(baseURL, webRootPathDefault)
+	webBasePath = path.Join(baseURL, webBasePathDefault)
+	webLoginPath = path.Join(baseURL, webLoginPathDefault)
+	webLogoutPath = path.Join(baseURL, webLogoutPathDefault)
+	webUsersPath = path.Join(baseURL, webUsersPathDefault)
+	webUserPath = path.Join(baseURL, webUserPathDefault)
+	webConnectionsPath = path.Join(baseURL, webConnectionsPathDefault)
+	webFoldersPath = path.Join(baseURL, webFoldersPathDefault)
+	webFolderPath = path.Join(baseURL, webFolderPathDefault)
+	webStatusPath = path.Join(baseURL, webStatusPathDefault)
+	webAdminsPath = path.Join(baseURL, webAdminsPathDefault)
+	webAdminPath = path.Join(baseURL, webAdminPathDefault)
+	webMaintenancePath = path.Join(baseURL, webMaintenancePathDefault)
+	webBackupPath = path.Join(baseURL, webBackupPathDefault)
+	webRestorePath = path.Join(baseURL, webRestorePathDefault)
+	webScanVFolderPath = path.Join(baseURL, webScanVFolderPathDefault)
+	webQuotaScanPath = path.Join(baseURL, webQuotaScanPathDefault)
+	webChangeAdminPwdPath = path.Join(baseURL, webChangeAdminPwdPathDefault)
+	webTemplateUser = path.Join(baseURL, webTemplateUserDefault)
+	webTemplateFolder = path.Join(baseURL, webTemplateFolderDefault)
+	webStaticFilesPath = path.Join(baseURL, webStaticFilesPathDefault)
+}
+
 // GetHTTPRouter returns an HTTP handler suitable to use for test cases
 func GetHTTPRouter() http.Handler {
 	b := Binding{

+ 2 - 1
httpd/server.go

@@ -257,6 +257,7 @@ func (s *httpdServer) initializeRouter() {
 
 	s.router.Use(saveConnectionAddress)
 	s.router.Use(middleware.GetHead)
+	s.router.Use(middleware.StripSlashes)
 
 	s.router.Group(func(r chi.Router) {
 		r.Get(healthzPath, func(w http.ResponseWriter, r *http.Request) {
@@ -334,7 +335,7 @@ func (s *httpdServer) initializeRouter() {
 		})
 
 		if s.enableWebAdmin {
-			router.Get("/", func(w http.ResponseWriter, r *http.Request) {
+			router.Get(webRootPath, func(w http.ResponseWriter, r *http.Request) {
 				http.Redirect(w, r, webLoginPath, http.StatusMovedPermanently)
 			})
 

+ 4 - 0
httpd/web.go

@@ -96,6 +96,7 @@ type basePage struct {
 	FolderQuotaScanURL string
 	StatusURL          string
 	MaintenanceURL     string
+	StaticURL          string
 	UsersTitle         string
 	AdminsTitle        string
 	ConnectionsTitle   string
@@ -182,6 +183,7 @@ type loginPage struct {
 	Version    string
 	Error      string
 	CSRFToken  string
+	StaticURL  string
 }
 
 type userTemplateFields struct {
@@ -290,6 +292,7 @@ func getBasePageData(title, currentURL string, r *http.Request) basePage {
 		StatusURL:          webStatusPath,
 		FolderQuotaScanURL: webScanVFolderPath,
 		MaintenanceURL:     webMaintenancePath,
+		StaticURL:          webStaticFilesPath,
 		UsersTitle:         pageUsersTitle,
 		AdminsTitle:        pageAdminsTitle,
 		ConnectionsTitle:   pageConnectionsTitle,
@@ -1030,6 +1033,7 @@ func renderLoginPage(w http.ResponseWriter, error string) {
 		Version:    version.Get().Version,
 		Error:      error,
 		CSRFToken:  createCSRFToken(),
+		StaticURL:  webStaticFilesPath,
 	}
 	renderTemplate(w, templateLogin, data)
 }

+ 1 - 0
sftpgo.json

@@ -165,6 +165,7 @@
     "templates_path": "templates",
     "static_files_path": "static",
     "backups_path": "backups",
+    "web_admin_root": "",
     "certificate_file": "",
     "certificate_key_file": "",
     "ca_certificates": [],

+ 0 - 20
static/css/fonts.css

@@ -1,20 +0,0 @@
-@font-face {
-    font-family: 'Roboto';
-    src: url('/static/vendor/fonts/Roboto-Bold-webfont.woff');
-    font-weight: 700;
-    font-style: normal;
-  }
-  
-  @font-face {
-    font-family: 'Roboto';
-    src: url('/static/vendor/fonts/Roboto-Regular-webfont.woff');
-    font-weight: 400;
-    font-style: normal;
-  }
-  
-  @font-face {
-    font-family: 'Roboto';
-    src: url('/static/vendor/fonts/Roboto-Light-webfont.woff');
-    font-weight: 300;
-    font-style: normal;
-  }

+ 14 - 14
templates/admins.html

@@ -3,11 +3,11 @@
 {{define "title"}}{{.Title}}{{end}}
 
 {{define "extra_css"}}
-<link href="/static/vendor/datatables/dataTables.bootstrap4.min.css" rel="stylesheet">
-<link href="/static/vendor/datatables/buttons.bootstrap4.min.css" rel="stylesheet">
-<link href="/static/vendor/datatables/fixedHeader.bootstrap4.min.css" rel="stylesheet">
-<link href="/static/vendor/datatables/responsive.bootstrap4.min.css" rel="stylesheet">
-<link href="/static/vendor/datatables/select.bootstrap4.min.css" rel="stylesheet">
+<link href="{{.StaticURL}}/vendor/datatables/dataTables.bootstrap4.min.css" rel="stylesheet">
+<link href="{{.StaticURL}}/vendor/datatables/buttons.bootstrap4.min.css" rel="stylesheet">
+<link href="{{.StaticURL}}/vendor/datatables/fixedHeader.bootstrap4.min.css" rel="stylesheet">
+<link href="{{.StaticURL}}/vendor/datatables/responsive.bootstrap4.min.css" rel="stylesheet">
+<link href="{{.StaticURL}}/vendor/datatables/select.bootstrap4.min.css" rel="stylesheet">
 {{end}}
 
 {{define "page_body"}}
@@ -83,15 +83,15 @@
 {{end}}
 
 {{define "extra_js"}}
-<script src="/static/vendor/datatables/jquery.dataTables.min.js"></script>
-<script src="/static/vendor/datatables/dataTables.bootstrap4.min.js"></script>
-<script src="/static/vendor/datatables/dataTables.buttons.min.js"></script>
-<script src="/static/vendor/datatables/buttons.bootstrap4.min.js"></script>
-<script src="/static/vendor/datatables/dataTables.fixedHeader.min.js"></script>
-<script src="/static/vendor/datatables/dataTables.responsive.min.js"></script>
-<script src="/static/vendor/datatables/responsive.bootstrap4.min.js"></script>
-<script src="/static/vendor/datatables/dataTables.select.min.js"></script>
-<script src="/static/vendor/datatables/ellipsis.js"></script>
+<script src="{{.StaticURL}}/vendor/datatables/jquery.dataTables.min.js"></script>
+<script src="{{.StaticURL}}/vendor/datatables/dataTables.bootstrap4.min.js"></script>
+<script src="{{.StaticURL}}/vendor/datatables/dataTables.buttons.min.js"></script>
+<script src="{{.StaticURL}}/vendor/datatables/buttons.bootstrap4.min.js"></script>
+<script src="{{.StaticURL}}/vendor/datatables/dataTables.fixedHeader.min.js"></script>
+<script src="{{.StaticURL}}/vendor/datatables/dataTables.responsive.min.js"></script>
+<script src="{{.StaticURL}}/vendor/datatables/responsive.bootstrap4.min.js"></script>
+<script src="{{.StaticURL}}/vendor/datatables/dataTables.select.min.js"></script>
+<script src="{{.StaticURL}}/vendor/datatables/ellipsis.js"></script>
 <script type="text/javascript">
 
     function deleteAction() {

+ 29 - 9
templates/base.html

@@ -12,16 +12,36 @@
 
     <title>SFTPGo - {{template "title" .}}</title>
 
-    <link rel="shortcut icon" href="/static/favicon.ico" />
+    <link rel="shortcut icon" href="{{.StaticURL}}/favicon.ico" />
 
     <!-- Custom fonts for this template-->
-    <link href="/static/vendor/fontawesome-free/css/fontawesome.min.css" rel="stylesheet" type="text/css">
-    <link href="/static/vendor/fontawesome-free/css/solid.min.css" rel="stylesheet" type="text/css">
-    <link href="/static/css/fonts.css" rel="stylesheet">
+    <link href="{{.StaticURL}}/vendor/fontawesome-free/css/fontawesome.min.css" rel="stylesheet" type="text/css">
+    <link href="{{.StaticURL}}/vendor/fontawesome-free/css/solid.min.css" rel="stylesheet" type="text/css">
 
     <!-- Custom styles for this template-->
-    <link href="/static/css/sb-admin-2.min.css" rel="stylesheet">
+    <link href="{{.StaticURL}}/css/sb-admin-2.min.css" rel="stylesheet">
     <style>
+        @font-face {
+            font-family: 'Roboto';
+            src: url('{{.StaticURL}}/vendor/fonts/Roboto-Bold-webfont.woff');
+            font-weight: 700;
+            font-style: normal;
+        }
+
+        @font-face {
+            font-family: 'Roboto';
+            src: url('{{.StaticURL}}/vendor/fonts/Roboto-Regular-webfont.woff');
+            font-weight: 400;
+            font-style: normal;
+        }
+
+        @font-face {
+            font-family: 'Roboto';
+            src: url('{{.StaticURL}}/vendor/fonts/Roboto-Light-webfont.woff');
+            font-weight: 300;
+            font-style: normal;
+        }
+
         div.dt-buttons {
             margin-bottom: 1em;
         }
@@ -212,14 +232,14 @@
     {{block "dialog" .}}{{end}}
 
     <!-- Bootstrap core JavaScript-->
-    <script src="/static/vendor/jquery/jquery.min.js"></script>
-    <script src="/static/vendor/bootstrap/js/bootstrap.bundle.min.js"></script>
+    <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="/static/vendor/jquery-easing/jquery.easing.min.js"></script>
+    <script src="{{.StaticURL}}/vendor/jquery-easing/jquery.easing.min.js"></script>
 
     <!-- Custom scripts for all pages-->
-    <script src="/static/js/sb-admin-2.min.js"></script>
+    <script src="{{.StaticURL}}/js/sb-admin-2.min.js"></script>
 
     <script type="text/javascript">
         function fixedEncodeURIComponent(str) {

+ 16 - 19
templates/connections.html

@@ -3,11 +3,11 @@
 {{define "title"}}{{.Title}}{{end}}
 
 {{define "extra_css"}}
-<link href="/static/vendor/datatables/dataTables.bootstrap4.min.css" rel="stylesheet">
-<link href="/static/vendor/datatables/buttons.bootstrap4.min.css" rel="stylesheet">
-<link href="/static/vendor/datatables/fixedHeader.bootstrap4.min.css" rel="stylesheet">
-<link href="/static/vendor/datatables/responsive.bootstrap4.min.css" rel="stylesheet">
-<link href="/static/vendor/datatables/select.bootstrap4.min.css" rel="stylesheet">
+<link href="{{.StaticURL}}/vendor/datatables/dataTables.bootstrap4.min.css" rel="stylesheet">
+<link href="{{.StaticURL}}/vendor/datatables/buttons.bootstrap4.min.css" rel="stylesheet">
+<link href="{{.StaticURL}}/vendor/datatables/fixedHeader.bootstrap4.min.css" rel="stylesheet">
+<link href="{{.StaticURL}}/vendor/datatables/responsive.bootstrap4.min.css" rel="stylesheet">
+<link href="{{.StaticURL}}/vendor/datatables/select.bootstrap4.min.css" rel="stylesheet">
 {{end}}
 
 {{define "page_body"}}
@@ -20,7 +20,6 @@
         <h6 class="m-0 font-weight-bold text-primary">View and manage connections</h6>
     </div>
     <div class="card-body">
-        {{if .Connections}}
         <div class="table-responsive">
             <table class="table table-striped table-bordered nowrap" id="dataTable" width="100%" cellspacing="0">
                 <thead>
@@ -45,11 +44,6 @@
                 </tbody>
             </table>
         </div>
-        {{else}}
-        <div class="card mb-2 border-left-info">
-            <div class="card-body">No user connected</div>
-        </div>
-        {{end}}
     </div>
 </div>
 {{end}}
@@ -82,14 +76,14 @@
 {{end}}
 
 {{define "extra_js"}}
-<script src="/static/vendor/datatables/jquery.dataTables.min.js"></script>
-<script src="/static/vendor/datatables/dataTables.bootstrap4.min.js"></script>
-<script src="/static/vendor/datatables/dataTables.buttons.min.js"></script>
-<script src="/static/vendor/datatables/buttons.bootstrap4.min.js"></script>
-<script src="/static/vendor/datatables/dataTables.fixedHeader.min.js"></script>
-<script src="/static/vendor/datatables/dataTables.responsive.min.js"></script>
-<script src="/static/vendor/datatables/responsive.bootstrap4.min.js"></script>
-<script src="/static/vendor/datatables/dataTables.select.min.js"></script>
+<script src="{{.StaticURL}}/vendor/datatables/jquery.dataTables.min.js"></script>
+<script src="{{.StaticURL}}/vendor/datatables/dataTables.bootstrap4.min.js"></script>
+<script src="{{.StaticURL}}/vendor/datatables/dataTables.buttons.min.js"></script>
+<script src="{{.StaticURL}}/vendor/datatables/buttons.bootstrap4.min.js"></script>
+<script src="{{.StaticURL}}/vendor/datatables/dataTables.fixedHeader.min.js"></script>
+<script src="{{.StaticURL}}/vendor/datatables/dataTables.responsive.min.js"></script>
+<script src="{{.StaticURL}}/vendor/datatables/responsive.bootstrap4.min.js"></script>
+<script src="{{.StaticURL}}/vendor/datatables/dataTables.select.min.js"></script>
 <script type="text/javascript">
 
     function disconnectAction() {
@@ -151,6 +145,9 @@
             "scrollX": false,
             "scrollY": false,
             "responsive": true,
+            "language": {
+                "emptyTable": "No user connected"
+            },
             "order": [[1, 'asc']]
         });
 

+ 17 - 14
templates/folders.html

@@ -3,11 +3,11 @@
 {{define "title"}}{{.Title}}{{end}}
 
 {{define "extra_css"}}
-<link href="/static/vendor/datatables/dataTables.bootstrap4.min.css" rel="stylesheet">
-<link href="/static/vendor/datatables/buttons.bootstrap4.min.css" rel="stylesheet">
-<link href="/static/vendor/datatables/fixedHeader.bootstrap4.min.css" rel="stylesheet">
-<link href="/static/vendor/datatables/responsive.bootstrap4.min.css" rel="stylesheet">
-<link href="/static/vendor/datatables/select.bootstrap4.min.css" rel="stylesheet">
+<link href="{{.StaticURL}}/vendor/datatables/dataTables.bootstrap4.min.css" rel="stylesheet">
+<link href="{{.StaticURL}}/vendor/datatables/buttons.bootstrap4.min.css" rel="stylesheet">
+<link href="{{.StaticURL}}/vendor/datatables/fixedHeader.bootstrap4.min.css" rel="stylesheet">
+<link href="{{.StaticURL}}/vendor/datatables/responsive.bootstrap4.min.css" rel="stylesheet">
+<link href="{{.StaticURL}}/vendor/datatables/select.bootstrap4.min.css" rel="stylesheet">
 {{end}}
 
 {{define "page_body"}}
@@ -81,15 +81,15 @@
 {{end}}
 
 {{define "extra_js"}}
-<script src="/static/vendor/datatables/jquery.dataTables.min.js"></script>
-<script src="/static/vendor/datatables/dataTables.bootstrap4.min.js"></script>
-<script src="/static/vendor/datatables/dataTables.buttons.min.js"></script>
-<script src="/static/vendor/datatables/buttons.bootstrap4.min.js"></script>
-<script src="/static/vendor/datatables/dataTables.fixedHeader.min.js"></script>
-<script src="/static/vendor/datatables/dataTables.responsive.min.js"></script>
-<script src="/static/vendor/datatables/responsive.bootstrap4.min.js"></script>
-<script src="/static/vendor/datatables/dataTables.select.min.js"></script>
-<script src="/static/vendor/datatables/ellipsis.js"></script>
+<script src="{{.StaticURL}}/vendor/datatables/jquery.dataTables.min.js"></script>
+<script src="{{.StaticURL}}/vendor/datatables/dataTables.bootstrap4.min.js"></script>
+<script src="{{.StaticURL}}/vendor/datatables/dataTables.buttons.min.js"></script>
+<script src="{{.StaticURL}}/vendor/datatables/buttons.bootstrap4.min.js"></script>
+<script src="{{.StaticURL}}/vendor/datatables/dataTables.fixedHeader.min.js"></script>
+<script src="{{.StaticURL}}/vendor/datatables/dataTables.responsive.min.js"></script>
+<script src="{{.StaticURL}}/vendor/datatables/responsive.bootstrap4.min.js"></script>
+<script src="{{.StaticURL}}/vendor/datatables/dataTables.select.min.js"></script>
+<script src="{{.StaticURL}}/vendor/datatables/ellipsis.js"></script>
 <script type="text/javascript">
 
 function deleteAction() {
@@ -241,6 +241,9 @@ function deleteAction() {
             "scrollX": false,
             "scrollY": false,
             "responsive": true,
+            "language": {
+                "emptyTable": "No folder defined"
+            },
             "order": [[0, 'asc']]
         });
 

+ 35 - 9
templates/login.html

@@ -11,14 +11,40 @@
 
     <title>SFTPGo - Login</title>
 
-    <link rel="shortcut icon" href="/static/favicon.ico" />
-
-    <!-- Custom fonts for this template-->
-    <link href="/static/css/fonts.css" rel="stylesheet">
+    <link rel="shortcut icon" href="{{.StaticURL}}/favicon.ico" />
 
     <!-- Custom styles for this template-->
-    <link href="/static/css/sb-admin-2.min.css" rel="stylesheet">
+    <link href="{{.StaticURL}}/css/sb-admin-2.min.css" rel="stylesheet">
     <style>
+        @font-face {
+            font-family: 'Roboto';
+            src: url('{{.StaticURL}}/vendor/fonts/Roboto-Bold-webfont.woff');
+            font-weight: 700;
+            font-style: normal;
+        }
+
+        @font-face {
+            font-family: 'Roboto';
+            src: url('{{.StaticURL}}/vendor/fonts/Roboto-Regular-webfont.woff');
+            font-weight: 400;
+            font-style: normal;
+        }
+
+        @font-face {
+            font-family: 'Roboto';
+            src: url('{{.StaticURL}}/vendor/fonts/Roboto-Light-webfont.woff');
+            font-weight: 300;
+            font-style: normal;
+        }
+
+        div.dt-buttons {
+            margin-bottom: 1em;
+        }
+
+        .text-form-error {
+            color: var(--red) !important;
+        }
+
         div.dt-buttons {
             margin-bottom: 1em;
         }
@@ -94,14 +120,14 @@
     </div>
 
     <!-- Bootstrap core JavaScript-->
-    <script src="/static/vendor/jquery/jquery.min.js"></script>
-    <script src="/static/vendor/bootstrap/js/bootstrap.bundle.min.js"></script>
+    <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="/static/vendor/jquery-easing/jquery.easing.min.js"></script>
+    <script src="{{.StaticURL}}/vendor/jquery-easing/jquery.easing.min.js"></script>
 
     <!-- Custom scripts for all pages-->
-    <script src="/static/js/sb-admin-2.min.js"></script>
+    <script src="{{.StaticURL}}/js/sb-admin-2.min.js"></script>
 
 </body>
 

+ 3 - 3
templates/user.html

@@ -3,7 +3,7 @@
 {{define "title"}}{{.Title}}{{end}}
 
 {{define "extra_css"}}
-<link href="/static/vendor/tempusdominus/css/tempusdominus-bootstrap-4.min.css" rel="stylesheet">
+<link href="{{.StaticURL}}/vendor/tempusdominus/css/tempusdominus-bootstrap-4.min.css" rel="stylesheet">
 {{end}}
 
 {{define "page_body"}}
@@ -423,8 +423,8 @@
 {{end}}
 
 {{define "extra_js"}}
-<script src="/static/vendor/moment/js/moment.min.js"></script>
-<script src="/static/vendor/tempusdominus/js/tempusdominus-bootstrap-4.min.js"></script>
+<script src="{{.StaticURL}}/vendor/moment/js/moment.min.js"></script>
+<script src="{{.StaticURL}}/vendor/tempusdominus/js/tempusdominus-bootstrap-4.min.js"></script>
 <script type="text/javascript">
     $(document).ready(function () {
 

+ 17 - 14
templates/users.html

@@ -3,11 +3,11 @@
 {{define "title"}}{{.Title}}{{end}}
 
 {{define "extra_css"}}
-<link href="/static/vendor/datatables/dataTables.bootstrap4.min.css" rel="stylesheet">
-<link href="/static/vendor/datatables/buttons.bootstrap4.min.css" rel="stylesheet">
-<link href="/static/vendor/datatables/fixedHeader.bootstrap4.min.css" rel="stylesheet">
-<link href="/static/vendor/datatables/responsive.bootstrap4.min.css" rel="stylesheet">
-<link href="/static/vendor/datatables/select.bootstrap4.min.css" rel="stylesheet">
+<link href="{{.StaticURL}}/vendor/datatables/dataTables.bootstrap4.min.css" rel="stylesheet">
+<link href="{{.StaticURL}}/vendor/datatables/buttons.bootstrap4.min.css" rel="stylesheet">
+<link href="{{.StaticURL}}/vendor/datatables/fixedHeader.bootstrap4.min.css" rel="stylesheet">
+<link href="{{.StaticURL}}/vendor/datatables/responsive.bootstrap4.min.css" rel="stylesheet">
+<link href="{{.StaticURL}}/vendor/datatables/select.bootstrap4.min.css" rel="stylesheet">
 {{end}}
 
 {{define "page_body"}}
@@ -84,15 +84,15 @@
 {{end}}
 
 {{define "extra_js"}}
-<script src="/static/vendor/datatables/jquery.dataTables.min.js"></script>
-<script src="/static/vendor/datatables/dataTables.bootstrap4.min.js"></script>
-<script src="/static/vendor/datatables/dataTables.buttons.min.js"></script>
-<script src="/static/vendor/datatables/buttons.bootstrap4.min.js"></script>
-<script src="/static/vendor/datatables/dataTables.fixedHeader.min.js"></script>
-<script src="/static/vendor/datatables/dataTables.responsive.min.js"></script>
-<script src="/static/vendor/datatables/responsive.bootstrap4.min.js"></script>
-<script src="/static/vendor/datatables/dataTables.select.min.js"></script>
-<script src="/static/vendor/datatables/ellipsis.js"></script>
+<script src="{{.StaticURL}}/vendor/datatables/jquery.dataTables.min.js"></script>
+<script src="{{.StaticURL}}/vendor/datatables/dataTables.bootstrap4.min.js"></script>
+<script src="{{.StaticURL}}/vendor/datatables/dataTables.buttons.min.js"></script>
+<script src="{{.StaticURL}}/vendor/datatables/buttons.bootstrap4.min.js"></script>
+<script src="{{.StaticURL}}/vendor/datatables/dataTables.fixedHeader.min.js"></script>
+<script src="{{.StaticURL}}/vendor/datatables/dataTables.responsive.min.js"></script>
+<script src="{{.StaticURL}}/vendor/datatables/responsive.bootstrap4.min.js"></script>
+<script src="{{.StaticURL}}/vendor/datatables/dataTables.select.min.js"></script>
+<script src="{{.StaticURL}}/vendor/datatables/ellipsis.js"></script>
 <script type="text/javascript">
 
     function deleteAction() {
@@ -262,6 +262,9 @@
             "scrollX": false,
             "scrollY": false,
             "responsive": true,
+            "language": {
+                "emptyTable": "No user defined"
+            },
             "order": [[1, 'asc']]
         });