From afd312f26a2c7765f5b420666debd5fe304bbf39 Mon Sep 17 00:00:00 2001 From: Nicola Murino Date: Mon, 7 Oct 2019 18:19:01 +0200 Subject: [PATCH] add a basic web interface The builtin web interface allows to manage users and connections --- README.md | 21 +- api/api.go | 78 - config/config.go | 14 +- config/config_test.go | 4 +- dataprovider/dataprovider.go | 31 +- dataprovider/user.go | 66 + api/quota.go => httpd/api_quota.go | 2 +- api/user.go => httpd/api_user.go | 2 +- {api => httpd}/api_utils.go | 29 +- httpd/httpd.go | 78 + api/api_test.go => httpd/httpd_test.go | 447 +- {api => httpd}/internal_test.go | 17 +- {api => httpd}/router.go | 55 +- {api => httpd}/schema/openapi.yaml | 6 +- httpd/web.go | 340 ++ service/service.go | 19 +- sftpd/internal_test.go | 43 + sftpd/sftpd.go | 41 + sftpd/sftpd_test.go | 228 +- sftpgo.json | 4 +- static/css/fonts.css | 20 + static/css/sb-admin-2.min.css | 10 + static/favicon.ico | Bin 0 -> 4286 bytes static/js/sb-admin-2.js | 49 + static/js/sb-admin-2.min.js | 7 + .../bootstrap/js/bootstrap.bundle.min.js | 7 + static/vendor/bootstrap/js/bootstrap.min.js | 7 + .../datatables/buttons.bootstrap4.min.css | 1 + .../datatables/buttons.bootstrap4.min.js | 6 + .../datatables/dataTables.bootstrap4.min.css | 1 + .../datatables/dataTables.bootstrap4.min.js | 11 + .../datatables/dataTables.buttons.min.js | 42 + .../datatables/dataTables.select.min.js | 38 + .../datatables/jquery.dataTables.min.js | 180 + .../datatables/select.bootstrap4.min.css | 1 + .../datatables/select.bootstrap4.min.js | 5 + .../vendor/fontawesome-free/css/all.min.css | 5 + static/vendor/fontawesome-free/package.json | 81 + .../svgs/solid/exchange-alt.svg | 1 + .../svgs/solid/folder-open.svg | 1 + .../fontawesome-free/svgs/solid/user.svg | 1 + .../webfonts/fa-solid-900.eot | Bin 0 -> 192122 bytes .../webfonts/fa-solid-900.svg | 4649 +++++++++++++++++ .../webfonts/fa-solid-900.ttf | Bin 0 -> 191836 bytes .../webfonts/fa-solid-900.woff | Bin 0 -> 98016 bytes .../webfonts/fa-solid-900.woff2 | Bin 0 -> 75408 bytes .../jquery.easing.compatibility.js | 59 + .../vendor/jquery-easing/jquery.easing.min.js | 1 + static/vendor/jquery/jquery.min.js | 2 + static/vendor/jquery/jquery.slim.min.js | 2 + templates/base.html | 131 + templates/connections.html | 161 + templates/message.html | 19 + templates/user.html | 135 + templates/users.html | 182 + utils/utils.go | 47 + 56 files changed, 7060 insertions(+), 327 deletions(-) delete mode 100644 api/api.go rename api/quota.go => httpd/api_quota.go (98%) rename api/user.go => httpd/api_user.go (99%) rename {api => httpd}/api_utils.go (93%) create mode 100644 httpd/httpd.go rename api/api_test.go => httpd/httpd_test.go (59%) rename {api => httpd}/internal_test.go (93%) rename {api => httpd}/router.go (60%) rename {api => httpd}/schema/openapi.yaml (98%) create mode 100644 httpd/web.go create mode 100644 static/css/fonts.css create mode 100644 static/css/sb-admin-2.min.css create mode 100644 static/favicon.ico create mode 100644 static/js/sb-admin-2.js create mode 100644 static/js/sb-admin-2.min.js create mode 100644 static/vendor/bootstrap/js/bootstrap.bundle.min.js create mode 100644 static/vendor/bootstrap/js/bootstrap.min.js create mode 100644 static/vendor/datatables/buttons.bootstrap4.min.css create mode 100644 static/vendor/datatables/buttons.bootstrap4.min.js create mode 100644 static/vendor/datatables/dataTables.bootstrap4.min.css create mode 100644 static/vendor/datatables/dataTables.bootstrap4.min.js create mode 100644 static/vendor/datatables/dataTables.buttons.min.js create mode 100644 static/vendor/datatables/dataTables.select.min.js create mode 100644 static/vendor/datatables/jquery.dataTables.min.js create mode 100644 static/vendor/datatables/select.bootstrap4.min.css create mode 100644 static/vendor/datatables/select.bootstrap4.min.js create mode 100644 static/vendor/fontawesome-free/css/all.min.css create mode 100644 static/vendor/fontawesome-free/package.json create mode 100644 static/vendor/fontawesome-free/svgs/solid/exchange-alt.svg create mode 100644 static/vendor/fontawesome-free/svgs/solid/folder-open.svg create mode 100644 static/vendor/fontawesome-free/svgs/solid/user.svg create mode 100644 static/vendor/fontawesome-free/webfonts/fa-solid-900.eot create mode 100644 static/vendor/fontawesome-free/webfonts/fa-solid-900.svg create mode 100644 static/vendor/fontawesome-free/webfonts/fa-solid-900.ttf create mode 100644 static/vendor/fontawesome-free/webfonts/fa-solid-900.woff create mode 100644 static/vendor/fontawesome-free/webfonts/fa-solid-900.woff2 create mode 100644 static/vendor/jquery-easing/jquery.easing.compatibility.js create mode 100644 static/vendor/jquery-easing/jquery.easing.min.js create mode 100644 static/vendor/jquery/jquery.min.js create mode 100644 static/vendor/jquery/jquery.slim.min.js create mode 100644 templates/base.html create mode 100644 templates/connections.html create mode 100644 templates/message.html create mode 100644 templates/user.html create mode 100644 templates/users.html diff --git a/README.md b/README.md index 029d50a5..aecc871c 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ Flags: -h, --help help for sftpgo -v, --version - Use "sftpgo [command] --help" for more information about a command + Use "sftpgo [command] --help" for more information about a command ``` The `serve` subcommand supports the following flags: @@ -125,7 +125,7 @@ The `sftpgo` configuration file contains the following sections: - `banner`, string. Identification string used by the server. Leave empty to use the default banner. Default "SFTPGo_version" - `upload_mode` integer. 0 means standard, the files are uploaded directly to the requested path. 1 means atomic: files are uploaded to a temporary path and renamed to the requested path when the client ends the upload. Atomic mode avoids problems such as a web server that serves partial files when the files are being uploaded. In atomic mode if there is an upload error the temporary file is deleted and so the requested upload path will not contain a partial file. - `actions`, struct. It contains the command to execute and/or the HTTP URL to notify and the trigger conditions - - `execute_on`, list of strings. Valid values are `download`, `upload`, `delete`, `rename`. On folder deletion a `delete` notification will be sent for each deleted file. Actions will be not executed if an error is detected and so a partial file is uploaded or downloaded. Leave empty to disable actions. The `upload` condition includes both uploads to new files and overwrite existing files + - `execute_on`, list of strings. Valid values are `download`, `upload`, `delete`, `rename`. On folder deletion a `delete` notification will be sent for each deleted file. Actions will be not executed if an error is detected and so a partial file is uploaded or downloaded. Leave empty to disable actions. The `upload` condition includes both uploads to new files and overwrite existing files - `command`, string. Absolute path to the command to execute. Leave empty to disable. The command is invoked with the following arguments: - `action`, any valid `execute_on` string - `username`, user who did the action @@ -163,6 +163,8 @@ The `sftpgo` configuration file contains the following sections: - **"httpd"**, the configuration for the HTTP server used to serve REST API - `bind_port`, integer. The port used for serving HTTP requests. Set to 0 to disable HTTP server. Default: 8080 - `bind_address`, string. Leave blank to listen on all available network interfaces. Default: "127.0.0.1" + - `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 Here is a full example showing the default config in JSON format: @@ -199,7 +201,9 @@ Here is a full example showing the default config in JSON format: }, "httpd": { "bind_port": 8080, - "bind_address": "127.0.0.1" + "bind_address": "127.0.0.1", + "templates_path": "templates", + "static_files_path": "static" } } ``` @@ -299,7 +303,7 @@ SFTPGo exposes REST API to manage users and quota and to get real time reports f If quota tracking is enabled in `sftpgo` configuration file, then the used size and number of files are updated each time a file is added/removed. If files are added/removed not using SFTP or if you change `track_quota` from `2` to `1`, you can rescan the user home dir and update the used quota using the REST API. -REST API is designed to run on localhost or on a trusted network, if you need HTTPS or authentication you can setup a reverse proxy using an HTTP Server such as Apache or NGNIX. +REST API is designed to run on localhost or on a trusted network, if you need HTTPS and/or authentication you can setup a reverse proxy using an HTTP Server such as Apache or NGNIX. For example you can keep SFTPGo listening on localhost and expose it externally configuring a reverse proxy using Apache HTTP Server this way: @@ -346,6 +350,15 @@ Several counters and gauges are available, for example: Please check the `/metrics` page for more details. +## Web Admin + +You can easily build your own interface using the exposed REST API, anyway SFTPGo provides also a very basic builtin web interface that allows to manage users and connections. +With the default `httpd` configuration, the web admin is available at the following URL: + +[http://127.0.0.1:8080/web](http://127.0.0.1:8080/web) + +If you need HTTPS and/or authentication you can setup a reverse proxy as explained for the REST API. + ## Logs Inside the log file each line is a JSON struct, each struct has a `sender` fields that identify the log type. diff --git a/api/api.go b/api/api.go deleted file mode 100644 index 1f00616c..00000000 --- a/api/api.go +++ /dev/null @@ -1,78 +0,0 @@ -// Package api implements REST API for sftpgo. -// REST API allows to manage users and quota and to get real time reports for the active connections -// with possibility of forcibly closing a connection. -// The OpenAPI 3 schema for the exposed API can be found inside the source tree: -// https://github.com/drakkan/sftpgo/tree/master/api/schema/openapi.yaml -package api - -import ( - "net/http" - - "github.com/drakkan/sftpgo/dataprovider" - "github.com/go-chi/chi" - "github.com/go-chi/render" -) - -const ( - logSender = "api" - activeConnectionsPath = "/api/v1/connection" - quotaScanPath = "/api/v1/quota_scan" - userPath = "/api/v1/user" - versionPath = "/api/v1/version" - metricsPath = "/metrics" -) - -var ( - router *chi.Mux - dataProvider dataprovider.Provider -) - -// HTTPDConf httpd daemon configuration -type HTTPDConf struct { - // The port used for serving HTTP requests. 0 disable the HTTP server. Default: 8080 - BindPort int `json:"bind_port" mapstructure:"bind_port"` - // The address to listen on. A blank value means listen on all available network interfaces. Default: "127.0.0.1" - BindAddress string `json:"bind_address" mapstructure:"bind_address"` -} - -type apiResponse struct { - Error string `json:"error"` - Message string `json:"message"` - HTTPStatus int `json:"status"` -} - -func init() { - initializeRouter() -} - -// SetDataProvider sets the data provider to use to fetch the data about users -func SetDataProvider(provider dataprovider.Provider) { - dataProvider = provider -} - -func sendAPIResponse(w http.ResponseWriter, r *http.Request, err error, message string, code int) { - var errorString string - if err != nil { - errorString = err.Error() - } - resp := apiResponse{ - Error: errorString, - Message: message, - HTTPStatus: code, - } - if code != http.StatusOK { - w.Header().Set("Content-Type", "application/json; charset=utf-8") - w.WriteHeader(code) - } - render.JSON(w, r, resp) -} - -func getRespStatus(err error) int { - if _, ok := err.(*dataprovider.ValidationError); ok { - return http.StatusBadRequest - } - if _, ok := err.(*dataprovider.MethodDisabledError); ok { - return http.StatusForbidden - } - return http.StatusInternalServerError -} diff --git a/config/config.go b/config/config.go index abcae8d3..f6800e15 100644 --- a/config/config.go +++ b/config/config.go @@ -9,8 +9,8 @@ import ( "fmt" "strings" - "github.com/drakkan/sftpgo/api" "github.com/drakkan/sftpgo/dataprovider" + "github.com/drakkan/sftpgo/httpd" "github.com/drakkan/sftpgo/logger" "github.com/drakkan/sftpgo/sftpd" "github.com/drakkan/sftpgo/utils" @@ -35,7 +35,7 @@ var ( type globalConfig struct { SFTPD sftpd.Configuration `json:"sftpd" mapstructure:"sftpd"` ProviderConf dataprovider.Config `json:"data_provider" mapstructure:"data_provider"` - HTTPDConfig api.HTTPDConf `json:"httpd" mapstructure:"httpd"` + HTTPDConfig httpd.Conf `json:"httpd" mapstructure:"httpd"` } func init() { @@ -76,9 +76,11 @@ func init() { PoolSize: 0, UsersBaseDir: "", }, - HTTPDConfig: api.HTTPDConf{ - BindPort: 8080, - BindAddress: "127.0.0.1", + HTTPDConfig: httpd.Conf{ + BindPort: 8080, + BindAddress: "127.0.0.1", + TemplatesPath: "templates", + StaticFilesPath: "static", }, } @@ -96,7 +98,7 @@ func GetSFTPDConfig() sftpd.Configuration { } // GetHTTPDConfig returns the configuration for the HTTP server -func GetHTTPDConfig() api.HTTPDConf { +func GetHTTPDConfig() httpd.Conf { return globalConf.HTTPDConfig } diff --git a/config/config_test.go b/config/config_test.go index d35f8186..553f01e5 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -8,9 +8,9 @@ import ( "strings" "testing" - "github.com/drakkan/sftpgo/api" "github.com/drakkan/sftpgo/config" "github.com/drakkan/sftpgo/dataprovider" + "github.com/drakkan/sftpgo/httpd" "github.com/drakkan/sftpgo/sftpd" ) @@ -24,7 +24,7 @@ func TestLoadConfigTest(t *testing.T) { if err != nil { t.Errorf("error loading config") } - emptyHTTPDConf := api.HTTPDConf{} + emptyHTTPDConf := httpd.Conf{} if config.GetHTTPDConfig() == emptyHTTPDConf { t.Errorf("error loading httpd conf") } diff --git a/dataprovider/dataprovider.go b/dataprovider/dataprovider.go index 082545e0..93d51e48 100644 --- a/dataprovider/dataprovider.go +++ b/dataprovider/dataprovider.go @@ -51,11 +51,12 @@ const ( var ( // SupportedProviders data provider configured in the sftpgo.conf file must match of these strings SupportedProviders = []string{SQLiteDataProviderName, PGSQLDataProviderName, MySQLDataProviderName, BoltDataProviderName} - config Config - provider Provider - sqlPlaceholders []string - validPerms = []string{PermAny, PermListItems, PermDownload, PermUpload, PermDelete, PermRename, - PermCreateDirs, PermCreateSymlinks, PermOverwrite} + // ValidPerms list that contains all the valid permissions for an user + ValidPerms = []string{PermAny, PermListItems, PermDownload, PermUpload, PermOverwrite, PermRename, PermDelete, + PermCreateDirs, PermCreateSymlinks} + config Config + provider Provider + sqlPlaceholders []string hashPwdPrefixes = []string{argonPwdPrefix, bcryptPwdPrefix, pbkdf2SHA1Prefix, pbkdf2SHA256Prefix, pbkdf2SHA512Prefix, sha512cryptPwdPrefix} pbkdfPwdPrefixes = []string{pbkdf2SHA1Prefix, pbkdf2SHA256Prefix, pbkdf2SHA512Prefix} @@ -276,13 +277,25 @@ func buildUserHomeDir(user *User) { } } +func validatePermissions(user *User) error { + for _, p := range user.Permissions { + if !utils.IsStringInSlice(p, ValidPerms) { + return &ValidationError{err: fmt.Sprintf("Invalid permission: %v", p)} + } + } + if utils.IsStringInSlice(PermAny, user.Permissions) { + user.Permissions = []string{PermAny} + } + return nil +} + func validateUser(user *User) error { buildUserHomeDir(user) if len(user.Username) == 0 || len(user.HomeDir) == 0 { return &ValidationError{err: "Mandatory parameters missing"} } if len(user.Password) == 0 && len(user.PublicKeys) == 0 { - return &ValidationError{err: "Please set password or at least a public_key"} + return &ValidationError{err: "Please set a password or at least a public_key"} } if len(user.Permissions) == 0 { return &ValidationError{err: "Please grant some permissions to this user"} @@ -290,10 +303,8 @@ func validateUser(user *User) error { if !filepath.IsAbs(user.HomeDir) { return &ValidationError{err: fmt.Sprintf("home_dir must be an absolute path, actual value: %v", user.HomeDir)} } - for _, p := range user.Permissions { - if !utils.IsStringInSlice(p, validPerms) { - return &ValidationError{err: fmt.Sprintf("Invalid permission: %v", p)} - } + if err := validatePermissions(user); err != nil { + return err } if len(user.Password) > 0 && !utils.IsStringPrefixInSlice(user.Password, hashPwdPrefixes) { pwd, err := argon2id.CreateHash(user.Password, argon2id.DefaultParams) diff --git a/dataprovider/user.go b/dataprovider/user.go index 8251d3ad..641d33d9 100644 --- a/dataprovider/user.go +++ b/dataprovider/user.go @@ -2,7 +2,9 @@ package dataprovider import ( "encoding/json" + "fmt" "path/filepath" + "strconv" "github.com/drakkan/sftpgo/utils" ) @@ -123,3 +125,67 @@ func (u *User) GetRelativePath(path string) string { } return "/" + filepath.ToSlash(rel) } + +// GetQuotaSummary returns used quota and limits if defined +func (u *User) GetQuotaSummary() string { + var result string + result = "Files: " + strconv.Itoa(u.UsedQuotaFiles) + if u.QuotaFiles > 0 { + result += "/" + strconv.Itoa(u.QuotaFiles) + } + if u.UsedQuotaSize > 0 || u.QuotaSize > 0 { + result += ". Size: " + utils.ByteCountSI(u.UsedQuotaSize) + if u.QuotaSize > 0 { + result += "/" + utils.ByteCountSI(u.QuotaSize) + } + } + return result +} + +// GetPermissionsAsString returns the user's permissions as comma separated string +func (u *User) GetPermissionsAsString() string { + var result string + for _, p := range u.Permissions { + if len(result) > 0 { + result += ", " + } + result += p + } + return result +} + +// GetBandwidthAsString returns bandwidth limits if defines +func (u *User) GetBandwidthAsString() string { + result := "Download: " + if u.DownloadBandwidth > 0 { + result += utils.ByteCountSI(u.DownloadBandwidth*1000) + "/s." + } else { + result += "ulimited." + } + result += " Upload: " + if u.UploadBandwidth > 0 { + result += utils.ByteCountSI(u.UploadBandwidth*1000) + "/s." + } else { + result += "ulimited." + } + return result +} + +// GetInfoString returns user's info as string. +// Number of public keys, max sessions, uid and gid are returned +func (u *User) GetInfoString() string { + var result string + if len(u.PublicKeys) > 0 { + result += fmt.Sprintf("Public keys: %v ", len(u.PublicKeys)) + } + if u.MaxSessions > 0 { + result += fmt.Sprintf("Max sessions: %v ", u.MaxSessions) + } + if u.UID > 0 { + result += fmt.Sprintf("UID: %v ", u.UID) + } + if u.GID > 0 { + result += fmt.Sprintf("GID: %v ", u.GID) + } + return result +} diff --git a/api/quota.go b/httpd/api_quota.go similarity index 98% rename from api/quota.go rename to httpd/api_quota.go index a8570d63..19f09154 100644 --- a/api/quota.go +++ b/httpd/api_quota.go @@ -1,4 +1,4 @@ -package api +package httpd import ( "net/http" diff --git a/api/user.go b/httpd/api_user.go similarity index 99% rename from api/user.go rename to httpd/api_user.go index 14d7a8c9..eae03b73 100644 --- a/api/user.go +++ b/httpd/api_user.go @@ -1,4 +1,4 @@ -package api +package httpd import ( "errors" diff --git a/api/api_utils.go b/httpd/api_utils.go similarity index 93% rename from api/api_utils.go rename to httpd/api_utils.go index 50c42cb5..d09e2d9d 100644 --- a/api/api_utils.go +++ b/httpd/api_utils.go @@ -1,4 +1,4 @@ -package api +package httpd import ( "bytes" @@ -42,6 +42,33 @@ func buildURLRelativeToBase(paths ...string) string { return fmt.Sprintf("%s/%s", strings.TrimRight(httpBaseURL, "/"), strings.TrimLeft(p, "/")) } +func sendAPIResponse(w http.ResponseWriter, r *http.Request, err error, message string, code int) { + var errorString string + if err != nil { + errorString = err.Error() + } + resp := apiResponse{ + Error: errorString, + Message: message, + HTTPStatus: code, + } + if code != http.StatusOK { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(code) + } + render.JSON(w, r, resp) +} + +func getRespStatus(err error) int { + if _, ok := err.(*dataprovider.ValidationError); ok { + return http.StatusBadRequest + } + if _, ok := err.(*dataprovider.MethodDisabledError); ok { + return http.StatusForbidden + } + return http.StatusInternalServerError +} + // AddUser adds a new user and checks the received HTTP Status code against expectedStatusCode. func AddUser(user dataprovider.User, expectedStatusCode int) (dataprovider.User, []byte, error) { var newUser dataprovider.User diff --git a/httpd/httpd.go b/httpd/httpd.go new file mode 100644 index 00000000..1f34b686 --- /dev/null +++ b/httpd/httpd.go @@ -0,0 +1,78 @@ +// Package httpd implements REST API and Web interface for SFTPGo. +// REST API allows to manage users and quota and to get real time reports for the active connections +// with possibility of forcibly closing a connection. +// The OpenAPI 3 schema for the exposed API can be found inside the source tree: +// https://github.com/drakkan/sftpgo/tree/master/api/schema/openapi.yaml +// A basic Web interface to manage users and connections is provided too +package httpd + +import ( + "fmt" + "net/http" + "path/filepath" + "time" + + "github.com/drakkan/sftpgo/dataprovider" + "github.com/drakkan/sftpgo/logger" + "github.com/go-chi/chi" +) + +const ( + logSender = "api" + activeConnectionsPath = "/api/v1/connection" + quotaScanPath = "/api/v1/quota_scan" + userPath = "/api/v1/user" + versionPath = "/api/v1/version" + metricsPath = "/metrics" +) + +var ( + router *chi.Mux + dataProvider dataprovider.Provider +) + +// Conf httpd daemon configuration +type Conf struct { + // The port used for serving HTTP requests. 0 disable the HTTP server. Default: 8080 + BindPort int `json:"bind_port" mapstructure:"bind_port"` + // The address to listen on. A blank value means listen on all available network interfaces. Default: "127.0.0.1" + BindAddress string `json:"bind_address" mapstructure:"bind_address"` + // Path to the HTML web templates. This can be an absolute path or a path relative to the config dir + TemplatesPath string `json:"templates_path" mapstructure:"templates_path"` + // Path to the static files for the web interface. This can be an absolute path or a path relative to the config dir + StaticFilesPath string `json:"static_files_path" mapstructure:"static_files_path"` +} + +type apiResponse struct { + Error string `json:"error"` + Message string `json:"message"` + HTTPStatus int `json:"status"` +} + +// SetDataProvider sets the data provider to use to fetch the data about users +func SetDataProvider(provider dataprovider.Provider) { + dataProvider = provider +} + +// Initialize the HTTP server +func (c Conf) Initialize(configDir string) error { + logger.Debug(logSender, "", "initializing HTTP server with config %+v", c) + staticFilesPath := c.StaticFilesPath + if !filepath.IsAbs(staticFilesPath) { + staticFilesPath = filepath.Join(configDir, staticFilesPath) + } + templatesPath := c.TemplatesPath + if !filepath.IsAbs(templatesPath) { + templatesPath = filepath.Join(configDir, templatesPath) + } + loadTemplates(templatesPath) + initializeRouter(staticFilesPath) + httpServer := &http.Server{ + Addr: fmt.Sprintf("%s:%d", c.BindAddress, c.BindPort), + Handler: router, + ReadTimeout: 300 * time.Second, + WriteTimeout: 300 * time.Second, + MaxHeaderBytes: 1 << 20, // 1MB + } + return httpServer.ListenAndServe() +} diff --git a/api/api_test.go b/httpd/httpd_test.go similarity index 59% rename from api/api_test.go rename to httpd/httpd_test.go index b9237396..f79c5383 100644 --- a/api/api_test.go +++ b/httpd/httpd_test.go @@ -1,4 +1,4 @@ -package api_test +package httpd_test import ( "bytes" @@ -7,10 +7,12 @@ import ( "net" "net/http" "net/http/httptest" + "net/url" "os" "path/filepath" "runtime" "strconv" + "strings" "testing" "time" @@ -20,11 +22,12 @@ import ( _ "github.com/mattn/go-sqlite3" "github.com/rs/zerolog" - "github.com/drakkan/sftpgo/api" "github.com/drakkan/sftpgo/config" "github.com/drakkan/sftpgo/dataprovider" + "github.com/drakkan/sftpgo/httpd" "github.com/drakkan/sftpgo/logger" "github.com/drakkan/sftpgo/sftpd" + "github.com/drakkan/sftpgo/utils" ) const ( @@ -37,13 +40,18 @@ const ( quotaScanPath = "/api/v1/quota_scan" versionPath = "/api/v1/version" metricsPath = "/metrics" + webBasePath = "/web" + webUsersPath = "/web/users" + webUserPath = "/web/user" + webConnectionsPath = "/web/connections" configDir = ".." ) var ( - defaultPerms = []string{dataprovider.PermAny} - homeBasePath string - testServer *httptest.Server + defaultPerms = []string{dataprovider.PermAny} + homeBasePath string + testServer *httptest.Server + providerDriverName string ) func TestMain(m *testing.M) { @@ -56,6 +64,7 @@ func TestMain(m *testing.M) { logger.InitLogger(logfilePath, 5, 1, 28, false, zerolog.DebugLevel) config.LoadConfig(configDir, "") providerConf := config.GetProviderConf() + providerDriverName = providerConf.Driver err := dataprovider.Initialize(providerConf, configDir) if err != nil { @@ -64,40 +73,33 @@ func TestMain(m *testing.M) { } dataProvider := dataprovider.GetProvider() httpdConf := config.GetHTTPDConfig() - router := api.GetHTTPRouter() httpdConf.BindPort = 8081 - api.SetBaseURL("http://127.0.0.1:8081") + httpd.SetBaseURL("http://127.0.0.1:8081") sftpd.SetDataProvider(dataProvider) - api.SetDataProvider(dataProvider) + httpd.SetDataProvider(dataProvider) go func() { - logger.Debug(logSender, "", "initializing HTTP server with config %+v", httpdConf) - s := &http.Server{ - Addr: fmt.Sprintf("%s:%d", httpdConf.BindAddress, httpdConf.BindPort), - Handler: router, - ReadTimeout: 300 * time.Second, - WriteTimeout: 300 * time.Second, - MaxHeaderBytes: 1 << 20, // 1MB - } - if err := s.ListenAndServe(); err != nil { - logger.Error(logSender, "", "could not start HTTP server: %v", err) - } + go func() { + if err := httpdConf.Initialize(configDir); err != nil { + logger.Error(logSender, "", "could not start HTTP server: %v", err) + } + }() }() - testServer = httptest.NewServer(api.GetHTTPRouter()) - defer testServer.Close() - waitTCPListening(fmt.Sprintf("%s:%d", httpdConf.BindAddress, httpdConf.BindPort)) + testServer = httptest.NewServer(httpd.GetHTTPRouter()) + defer testServer.Close() + exitCode := m.Run() os.Remove(logfilePath) os.Exit(exitCode) } func TestBasicUserHandling(t *testing.T) { - user, _, err := api.AddUser(getTestUser(), http.StatusOK) + user, _, err := httpd.AddUser(getTestUser(), http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) } @@ -106,18 +108,18 @@ func TestBasicUserHandling(t *testing.T) { user.QuotaFiles = 2 user.UploadBandwidth = 128 user.DownloadBandwidth = 64 - user, _, err = api.UpdateUser(user, http.StatusOK) + user, _, err = httpd.UpdateUser(user, http.StatusOK) if err != nil { t.Errorf("unable to update user: %v", err) } - users, _, err := api.GetUsers(0, 0, defaultUsername, http.StatusOK) + users, _, err := httpd.GetUsers(0, 0, defaultUsername, http.StatusOK) if err != nil { t.Errorf("unable to get users: %v", err) } if len(users) != 1 { t.Errorf("number of users mismatch, expected: 1, actual: %v", len(users)) } - _, err = api.RemoveUser(user, http.StatusOK) + _, err = httpd.RemoveUser(user, http.StatusOK) if err != nil { t.Errorf("unable to remove: %v", err) } @@ -127,7 +129,7 @@ func TestAddUserNoCredentials(t *testing.T) { u := getTestUser() u.Password = "" u.PublicKeys = []string{} - _, _, err := api.AddUser(u, http.StatusBadRequest) + _, _, err := httpd.AddUser(u, http.StatusBadRequest) if err != nil { t.Errorf("unexpected error adding user with no credentials: %v", err) } @@ -136,7 +138,7 @@ func TestAddUserNoCredentials(t *testing.T) { func TestAddUserNoUsername(t *testing.T) { u := getTestUser() u.Username = "" - _, _, err := api.AddUser(u, http.StatusBadRequest) + _, _, err := httpd.AddUser(u, http.StatusBadRequest) if err != nil { t.Errorf("unexpected error adding user with no home dir: %v", err) } @@ -145,7 +147,7 @@ func TestAddUserNoUsername(t *testing.T) { func TestAddUserNoHomeDir(t *testing.T) { u := getTestUser() u.HomeDir = "" - _, _, err := api.AddUser(u, http.StatusBadRequest) + _, _, err := httpd.AddUser(u, http.StatusBadRequest) if err != nil { t.Errorf("unexpected error adding user with no home dir: %v", err) } @@ -154,7 +156,7 @@ func TestAddUserNoHomeDir(t *testing.T) { func TestAddUserInvalidHomeDir(t *testing.T) { u := getTestUser() u.HomeDir = "relative_path" - _, _, err := api.AddUser(u, http.StatusBadRequest) + _, _, err := httpd.AddUser(u, http.StatusBadRequest) if err != nil { t.Errorf("unexpected error adding user with invalid home dir: %v", err) } @@ -163,7 +165,7 @@ func TestAddUserInvalidHomeDir(t *testing.T) { func TestAddUserNoPerms(t *testing.T) { u := getTestUser() u.Permissions = []string{} - _, _, err := api.AddUser(u, http.StatusBadRequest) + _, _, err := httpd.AddUser(u, http.StatusBadRequest) if err != nil { t.Errorf("unexpected error adding user with no perms: %v", err) } @@ -172,7 +174,7 @@ func TestAddUserNoPerms(t *testing.T) { func TestAddUserInvalidPerms(t *testing.T) { u := getTestUser() u.Permissions = []string{"invalidPerm"} - _, _, err := api.AddUser(u, http.StatusBadRequest) + _, _, err := httpd.AddUser(u, http.StatusBadRequest) if err != nil { t.Errorf("unexpected error adding user with no perms: %v", err) } @@ -183,33 +185,33 @@ func TestUserPublicKey(t *testing.T) { invalidPubKey := "invalid" validPubKey := "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC03jj0D+djk7pxIf/0OhrxrchJTRZklofJ1NoIu4752Sq02mdXmarMVsqJ1cAjV5LBVy3D1F5U6XW4rppkXeVtd04Pxb09ehtH0pRRPaoHHlALiJt8CoMpbKYMA8b3KXPPriGxgGomvtU2T2RMURSwOZbMtpsugfjYSWenyYX+VORYhylWnSXL961LTyC21ehd6d6QnW9G7E5hYMITMY9TuQZz3bROYzXiTsgN0+g6Hn7exFQp50p45StUMfV/SftCMdCxlxuyGny2CrN/vfjO7xxOo2uv7q1qm10Q46KPWJQv+pgZ/OfL+EDjy07n5QVSKHlbx+2nT4Q0EgOSQaCTYwn3YjtABfIxWwgAFdyj6YlPulCL22qU4MYhDcA6PSBwDdf8hvxBfvsiHdM+JcSHvv8/VeJhk6CmnZxGY0fxBupov27z3yEO8nAg8k+6PaUiW1MSUfuGMF/ktB8LOstXsEPXSszuyXiOv4DaryOXUiSn7bmRqKcEFlJusO6aZP0= nicola@p1" u.PublicKeys = []string{invalidPubKey} - _, _, err := api.AddUser(u, http.StatusBadRequest) + _, _, err := httpd.AddUser(u, http.StatusBadRequest) if err != nil { t.Errorf("unexpected error adding user with invalid pub key: %v", err) } u.PublicKeys = []string{validPubKey} - user, _, err := api.AddUser(u, http.StatusOK) + user, _, err := httpd.AddUser(u, http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) } user.PublicKeys = []string{validPubKey, invalidPubKey} - _, _, err = api.UpdateUser(user, http.StatusBadRequest) + _, _, err = httpd.UpdateUser(user, http.StatusBadRequest) if err != nil { t.Errorf("update user with invalid public key must fail: %v", err) } user.PublicKeys = []string{validPubKey, validPubKey, validPubKey} - _, _, err = api.UpdateUser(user, http.StatusOK) + _, _, err = httpd.UpdateUser(user, http.StatusOK) if err != nil { t.Errorf("unable to update user: %v", err) } - _, err = api.RemoveUser(user, http.StatusOK) + _, err = httpd.RemoveUser(user, http.StatusOK) if err != nil { t.Errorf("unable to remove: %v", err) } } func TestUpdateUser(t *testing.T) { - user, _, err := api.AddUser(getTestUser(), http.StatusOK) + user, _, err := httpd.AddUser(getTestUser(), http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) } @@ -222,18 +224,18 @@ func TestUpdateUser(t *testing.T) { user.Permissions = []string{dataprovider.PermCreateDirs, dataprovider.PermDelete, dataprovider.PermDownload} user.UploadBandwidth = 1024 user.DownloadBandwidth = 512 - user, _, err = api.UpdateUser(user, http.StatusOK) + user, _, err = httpd.UpdateUser(user, http.StatusOK) if err != nil { t.Errorf("unable to update user: %v", err) } - _, err = api.RemoveUser(user, http.StatusOK) + _, err = httpd.RemoveUser(user, http.StatusOK) if err != nil { t.Errorf("unable to remove: %v", err) } } func TestUpdateUserNoCredentials(t *testing.T) { - user, _, err := api.AddUser(getTestUser(), http.StatusOK) + user, _, err := httpd.AddUser(getTestUser(), http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) } @@ -241,184 +243,184 @@ func TestUpdateUserNoCredentials(t *testing.T) { user.PublicKeys = []string{} // password and public key will be omitted from json serialization if empty and so they will remain unchanged // and no validation error will be raised - _, _, err = api.UpdateUser(user, http.StatusOK) + _, _, err = httpd.UpdateUser(user, http.StatusOK) if err != nil { t.Errorf("unexpected error updating user with no credentials: %v", err) } - _, err = api.RemoveUser(user, http.StatusOK) + _, err = httpd.RemoveUser(user, http.StatusOK) if err != nil { t.Errorf("unable to remove: %v", err) } } func TestUpdateUserEmptyHomeDir(t *testing.T) { - user, _, err := api.AddUser(getTestUser(), http.StatusOK) + user, _, err := httpd.AddUser(getTestUser(), http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) } user.HomeDir = "" - _, _, err = api.UpdateUser(user, http.StatusBadRequest) + _, _, err = httpd.UpdateUser(user, http.StatusBadRequest) if err != nil { t.Errorf("unexpected error updating user with empty home dir: %v", err) } - _, err = api.RemoveUser(user, http.StatusOK) + _, err = httpd.RemoveUser(user, http.StatusOK) if err != nil { t.Errorf("unable to remove: %v", err) } } func TestUpdateUserInvalidHomeDir(t *testing.T) { - user, _, err := api.AddUser(getTestUser(), http.StatusOK) + user, _, err := httpd.AddUser(getTestUser(), http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) } user.HomeDir = "relative_path" - _, _, err = api.UpdateUser(user, http.StatusBadRequest) + _, _, err = httpd.UpdateUser(user, http.StatusBadRequest) if err != nil { t.Errorf("unexpected error updating user with empty home dir: %v", err) } - _, err = api.RemoveUser(user, http.StatusOK) + _, err = httpd.RemoveUser(user, http.StatusOK) if err != nil { t.Errorf("unable to remove: %v", err) } } func TestUpdateNonExistentUser(t *testing.T) { - _, _, err := api.UpdateUser(getTestUser(), http.StatusNotFound) + _, _, err := httpd.UpdateUser(getTestUser(), http.StatusNotFound) if err != nil { t.Errorf("unable to update user: %v", err) } } func TestGetNonExistentUser(t *testing.T) { - _, _, err := api.GetUserByID(0, http.StatusNotFound) + _, _, err := httpd.GetUserByID(0, http.StatusNotFound) if err != nil { t.Errorf("unable to get user: %v", err) } } func TestDeleteNonExistentUser(t *testing.T) { - _, err := api.RemoveUser(getTestUser(), http.StatusNotFound) + _, err := httpd.RemoveUser(getTestUser(), http.StatusNotFound) if err != nil { t.Errorf("unable to remove user: %v", err) } } func TestAddDuplicateUser(t *testing.T) { - user, _, err := api.AddUser(getTestUser(), http.StatusOK) + user, _, err := httpd.AddUser(getTestUser(), http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) } - _, _, err = api.AddUser(getTestUser(), http.StatusInternalServerError) + _, _, err = httpd.AddUser(getTestUser(), http.StatusInternalServerError) if err != nil { t.Errorf("unable to add second user: %v", err) } - _, _, err = api.AddUser(getTestUser(), http.StatusOK) + _, _, err = httpd.AddUser(getTestUser(), http.StatusOK) if err == nil { t.Errorf("adding a duplicate user must fail") } - _, err = api.RemoveUser(user, http.StatusOK) + _, err = httpd.RemoveUser(user, http.StatusOK) if err != nil { t.Errorf("unable to remove user: %v", err) } } func TestGetUsers(t *testing.T) { - user1, _, err := api.AddUser(getTestUser(), http.StatusOK) + user1, _, err := httpd.AddUser(getTestUser(), http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) } u := getTestUser() u.Username = defaultUsername + "1" - user2, _, err := api.AddUser(u, http.StatusOK) + user2, _, err := httpd.AddUser(u, http.StatusOK) if err != nil { t.Errorf("unable to add second user: %v", err) } - users, _, err := api.GetUsers(0, 0, "", http.StatusOK) + users, _, err := httpd.GetUsers(0, 0, "", http.StatusOK) if err != nil { t.Errorf("unable to get users: %v", err) } if len(users) < 2 { t.Errorf("at least 2 users are expected") } - users, _, err = api.GetUsers(1, 0, "", http.StatusOK) + users, _, err = httpd.GetUsers(1, 0, "", http.StatusOK) if err != nil { t.Errorf("unable to get users: %v", err) } if len(users) != 1 { t.Errorf("1 user is expected") } - users, _, err = api.GetUsers(1, 1, "", http.StatusOK) + users, _, err = httpd.GetUsers(1, 1, "", http.StatusOK) if err != nil { t.Errorf("unable to get users: %v", err) } if len(users) != 1 { t.Errorf("1 user is expected") } - _, _, err = api.GetUsers(1, 1, "", http.StatusInternalServerError) + _, _, err = httpd.GetUsers(1, 1, "", http.StatusInternalServerError) if err == nil { t.Errorf("get users must succeed, we requested a fail for a good request") } - _, err = api.RemoveUser(user1, http.StatusOK) + _, err = httpd.RemoveUser(user1, http.StatusOK) if err != nil { t.Errorf("unable to remove user: %v", err) } - _, err = api.RemoveUser(user2, http.StatusOK) + _, err = httpd.RemoveUser(user2, http.StatusOK) if err != nil { t.Errorf("unable to remove user: %v", err) } } func TestGetQuotaScans(t *testing.T) { - _, _, err := api.GetQuotaScans(http.StatusOK) + _, _, err := httpd.GetQuotaScans(http.StatusOK) if err != nil { t.Errorf("unable to get quota scans: %v", err) } - _, _, err = api.GetQuotaScans(http.StatusInternalServerError) + _, _, err = httpd.GetQuotaScans(http.StatusInternalServerError) if err == nil { t.Errorf("quota scan request must succeed, we requested to check a wrong status code") } } func TestStartQuotaScan(t *testing.T) { - user, _, err := api.AddUser(getTestUser(), http.StatusOK) + user, _, err := httpd.AddUser(getTestUser(), http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) } - _, err = api.StartQuotaScan(user, http.StatusCreated) + _, err = httpd.StartQuotaScan(user, http.StatusCreated) if err != nil { t.Errorf("unable to start quota scan: %v", err) } - _, err = api.RemoveUser(user, http.StatusOK) + _, err = httpd.RemoveUser(user, http.StatusOK) if err != nil { t.Errorf("unable to remove user: %v", err) } } func TestGetVersion(t *testing.T) { - _, _, err := api.GetVersion(http.StatusOK) + _, _, err := httpd.GetVersion(http.StatusOK) if err != nil { t.Errorf("unable to get sftp version: %v", err) } - _, _, err = api.GetVersion(http.StatusInternalServerError) + _, _, err = httpd.GetVersion(http.StatusInternalServerError) if err == nil { t.Errorf("get version request must succeed, we requested to check a wrong status code") } } func TestGetConnections(t *testing.T) { - _, _, err := api.GetConnections(http.StatusOK) + _, _, err := httpd.GetConnections(http.StatusOK) if err != nil { t.Errorf("unable to get sftp connections: %v", err) } - _, _, err = api.GetConnections(http.StatusInternalServerError) + _, _, err = httpd.GetConnections(http.StatusInternalServerError) if err == nil { t.Errorf("get sftp connections request must succeed, we requested to check a wrong status code") } } func TestCloseActiveConnection(t *testing.T) { - _, err := api.CloseConnection("non_existent_id", http.StatusNotFound) + _, err := httpd.CloseConnection("non_existent_id", http.StatusNotFound) if err != nil { t.Errorf("unexpected error closing non existent sftp connection: %v", err) } @@ -434,17 +436,17 @@ func TestUserBaseDir(t *testing.T) { if err != nil { t.Errorf("error initializing data provider with users base dir") } - api.SetDataProvider(dataprovider.GetProvider()) + httpd.SetDataProvider(dataprovider.GetProvider()) u := getTestUser() u.HomeDir = "" - user, _, err := api.AddUser(getTestUser(), http.StatusOK) + user, _, err := httpd.AddUser(getTestUser(), http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) } if user.HomeDir != filepath.Join(providerConf.UsersBaseDir, u.Username) { t.Errorf("invalid home dir: %v", user.HomeDir) } - _, err = api.RemoveUser(user, http.StatusOK) + _, err = httpd.RemoveUser(user, http.StatusOK) if err != nil { t.Errorf("unable to remove: %v", err) } @@ -456,26 +458,29 @@ func TestUserBaseDir(t *testing.T) { if err != nil { t.Errorf("error initializing data provider") } - api.SetDataProvider(dataprovider.GetProvider()) + httpd.SetDataProvider(dataprovider.GetProvider()) sftpd.SetDataProvider(dataprovider.GetProvider()) } func TestProviderErrors(t *testing.T) { + if providerDriverName == dataprovider.BoltDataProviderName { + t.Skip("skipping test provider errors for bolt provider") + } dataProvider := dataprovider.GetProvider() dataprovider.Close(dataProvider) - _, _, err := api.GetUserByID(0, http.StatusInternalServerError) + _, _, err := httpd.GetUserByID(0, http.StatusInternalServerError) if err != nil { t.Errorf("get user with provider closed must fail: %v", err) } - _, _, err = api.GetUsers(0, 0, defaultUsername, http.StatusInternalServerError) + _, _, err = httpd.GetUsers(1, 0, defaultUsername, http.StatusInternalServerError) if err != nil { t.Errorf("get users with provider closed must fail: %v", err) } - _, _, err = api.UpdateUser(dataprovider.User{}, http.StatusInternalServerError) + _, _, err = httpd.UpdateUser(dataprovider.User{}, http.StatusInternalServerError) if err != nil { t.Errorf("update user with provider closed must fail: %v", err) } - _, err = api.RemoveUser(dataprovider.User{}, http.StatusInternalServerError) + _, err = httpd.RemoveUser(dataprovider.User{}, http.StatusInternalServerError) if err != nil { t.Errorf("delete user with provider closed must fail: %v", err) } @@ -485,7 +490,7 @@ func TestProviderErrors(t *testing.T) { if err != nil { t.Errorf("error initializing data provider") } - api.SetDataProvider(dataprovider.GetProvider()) + httpd.SetDataProvider(dataprovider.GetProvider()) sftpd.SetDataProvider(dataprovider.GetProvider()) } @@ -506,6 +511,7 @@ func TestBasicUserHandlingMock(t *testing.T) { checkResponseCode(t, http.StatusInternalServerError, rr.Code) user.MaxSessions = 10 user.UploadBandwidth = 128 + user.Permissions = []string{dataprovider.PermAny, dataprovider.PermDelete, dataprovider.PermDownload} userAsJSON = getUserAsJSON(t, user) req, _ = http.NewRequest(http.MethodPut, userPath+"/"+strconv.FormatInt(user.ID, 10), bytes.NewBuffer(userAsJSON)) rr = executeRequest(req) @@ -523,6 +529,12 @@ func TestBasicUserHandlingMock(t *testing.T) { if user.MaxSessions != updatedUser.MaxSessions || user.UploadBandwidth != updatedUser.UploadBandwidth { t.Errorf("Error modifying user actual: %v, %v", updatedUser.MaxSessions, updatedUser.UploadBandwidth) } + if len(updatedUser.Permissions) != 1 { + t.Errorf("permissions other than any should be removed") + } + if !utils.IsStringInSlice(dataprovider.PermAny, updatedUser.Permissions) { + t.Errorf("permissions mismatch") + } req, _ = http.NewRequest(http.MethodDelete, userPath+"/"+strconv.FormatInt(user.ID, 10), nil) rr = executeRequest(req) checkResponseCode(t, http.StatusOK, rr.Code) @@ -782,6 +794,275 @@ func TestMetricsMock(t *testing.T) { checkResponseCode(t, http.StatusOK, rr.Code) } +func TestGetWebRootMock(t *testing.T) { + req, _ := http.NewRequest(http.MethodGet, "/", nil) + rr := executeRequest(req) + checkResponseCode(t, http.StatusMovedPermanently, rr.Code) + req, _ = http.NewRequest(http.MethodGet, webBasePath, nil) + rr = executeRequest(req) + checkResponseCode(t, http.StatusMovedPermanently, rr.Code) +} + +func TestBasicWebUsersMock(t *testing.T) { + user := getTestUser() + userAsJSON := getUserAsJSON(t, user) + req, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON)) + rr := executeRequest(req) + checkResponseCode(t, http.StatusOK, rr.Code) + err := render.DecodeJSON(rr.Body, &user) + if err != nil { + t.Errorf("Error get user: %v", err) + } + user1 := getTestUser() + user1.Username += "1" + user1AsJSON := getUserAsJSON(t, user1) + req, _ = http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(user1AsJSON)) + rr = executeRequest(req) + checkResponseCode(t, http.StatusOK, rr.Code) + err = render.DecodeJSON(rr.Body, &user1) + if err != nil { + t.Errorf("Error get user1: %v", err) + } + req, _ = http.NewRequest(http.MethodGet, webUsersPath, nil) + rr = executeRequest(req) + checkResponseCode(t, http.StatusOK, rr.Code) + req, _ = http.NewRequest(http.MethodGet, webUsersPath+"?qlimit=a", nil) + rr = executeRequest(req) + checkResponseCode(t, http.StatusOK, rr.Code) + req, _ = http.NewRequest(http.MethodGet, webUsersPath+"?qlimit=1", nil) + rr = executeRequest(req) + checkResponseCode(t, http.StatusOK, rr.Code) + req, _ = http.NewRequest(http.MethodGet, webUserPath, nil) + rr = executeRequest(req) + checkResponseCode(t, http.StatusOK, rr.Code) + req, _ = http.NewRequest(http.MethodGet, webUserPath+"/"+strconv.FormatInt(user.ID, 10), nil) + rr = executeRequest(req) + checkResponseCode(t, http.StatusOK, rr.Code) + req, _ = http.NewRequest(http.MethodGet, webUserPath+"/0", nil) + rr = executeRequest(req) + checkResponseCode(t, http.StatusNotFound, rr.Code) + req, _ = http.NewRequest(http.MethodGet, webUserPath+"/a", nil) + rr = executeRequest(req) + checkResponseCode(t, http.StatusBadRequest, rr.Code) + form := make(url.Values) + form.Set("username", user.Username) + req, _ = http.NewRequest(http.MethodPost, webUserPath, strings.NewReader(form.Encode())) + rr = executeRequest(req) + checkResponseCode(t, http.StatusOK, rr.Code) + req, _ = http.NewRequest(http.MethodPost, webUserPath+"/"+strconv.FormatInt(user.ID, 10), strings.NewReader(form.Encode())) + rr = executeRequest(req) + checkResponseCode(t, http.StatusOK, rr.Code) + req, _ = http.NewRequest(http.MethodPost, webUserPath+"/0", strings.NewReader(form.Encode())) + rr = executeRequest(req) + checkResponseCode(t, http.StatusNotFound, rr.Code) + req, _ = http.NewRequest(http.MethodPost, webUserPath+"/a", strings.NewReader(form.Encode())) + rr = executeRequest(req) + checkResponseCode(t, http.StatusBadRequest, rr.Code) + req, _ = http.NewRequest(http.MethodDelete, userPath+"/"+strconv.FormatInt(user.ID, 10), nil) + rr = executeRequest(req) + checkResponseCode(t, http.StatusOK, rr.Code) + req, _ = http.NewRequest(http.MethodDelete, userPath+"/"+strconv.FormatInt(user1.ID, 10), nil) + rr = executeRequest(req) + checkResponseCode(t, http.StatusOK, rr.Code) +} + +func TestWebUserAddMock(t *testing.T) { + user := getTestUser() + user.UploadBandwidth = 32 + user.DownloadBandwidth = 64 + user.UID = 1000 + form := make(url.Values) + form.Set("username", user.Username) + form.Set("home_dir", user.HomeDir) + form.Set("password", user.Password) + form.Set("permissions", "*") + // test invalid url escape + req, _ := http.NewRequest(http.MethodPost, webUserPath+"?a=%2", strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + rr := executeRequest(req) + checkResponseCode(t, http.StatusOK, rr.Code) + form.Set("public_keys", testPubKey) + form.Set("uid", strconv.FormatInt(int64(user.UID), 10)) + form.Set("gid", "a") + // test invalid gid + req, _ = http.NewRequest(http.MethodPost, webUserPath, strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + rr = executeRequest(req) + checkResponseCode(t, http.StatusOK, rr.Code) + form.Set("gid", "0") + form.Set("max_sessions", "a") + // test invalid max sessions + req, _ = http.NewRequest(http.MethodPost, webUserPath, strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + rr = executeRequest(req) + checkResponseCode(t, http.StatusOK, rr.Code) + form.Set("max_sessions", "0") + form.Set("quota_size", "a") + // test invalid quota size + req, _ = http.NewRequest(http.MethodPost, webUserPath, strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + rr = executeRequest(req) + checkResponseCode(t, http.StatusOK, rr.Code) + form.Set("quota_size", "0") + form.Set("quota_files", "a") + // test invalid quota files + req, _ = http.NewRequest(http.MethodPost, webUserPath, strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + rr = executeRequest(req) + checkResponseCode(t, http.StatusOK, rr.Code) + form.Set("quota_files", "0") + form.Set("upload_bandwidth", "a") + // test invalid upload bandwidth + req, _ = http.NewRequest(http.MethodPost, webUserPath, strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + rr = executeRequest(req) + checkResponseCode(t, http.StatusOK, rr.Code) + form.Set("upload_bandwidth", strconv.FormatInt(user.UploadBandwidth, 10)) + form.Set("download_bandwidth", "a") + // test invalid download bandwidth + req, _ = http.NewRequest(http.MethodPost, webUserPath, strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + rr = executeRequest(req) + checkResponseCode(t, http.StatusOK, rr.Code) + form.Set("download_bandwidth", strconv.FormatInt(user.DownloadBandwidth, 10)) + req, _ = http.NewRequest(http.MethodPost, webUserPath, strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + rr = executeRequest(req) + checkResponseCode(t, http.StatusSeeOther, rr.Code) + // the user already exists, was created with the above request + req, _ = http.NewRequest(http.MethodPost, webUserPath, strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + rr = executeRequest(req) + checkResponseCode(t, http.StatusOK, rr.Code) + req, _ = http.NewRequest(http.MethodGet, userPath+"?limit=1&offset=0&order=ASC&username="+user.Username, nil) + rr = executeRequest(req) + checkResponseCode(t, http.StatusOK, rr.Code) + var users []dataprovider.User + err := render.DecodeJSON(rr.Body, &users) + if err != nil { + t.Errorf("Error decoding users: %v", err) + } + if len(users) != 1 { + t.Errorf("1 user is expected") + } + newUser := users[0] + if newUser.UID != user.UID { + t.Errorf("uid does not match") + } + if newUser.UploadBandwidth != user.UploadBandwidth { + t.Errorf("upload_bandwidth does not match") + } + if newUser.DownloadBandwidth != user.DownloadBandwidth { + t.Errorf("download_bandwidth does not match") + } + if !utils.IsStringInSlice(testPubKey, newUser.PublicKeys) { + t.Errorf("public_keys does not match") + } + req, _ = http.NewRequest(http.MethodDelete, userPath+"/"+strconv.FormatInt(newUser.ID, 10), nil) + rr = executeRequest(req) + checkResponseCode(t, http.StatusOK, rr.Code) +} + +func TestWebUserUpdateMock(t *testing.T) { + user := getTestUser() + userAsJSON := getUserAsJSON(t, user) + req, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON)) + rr := executeRequest(req) + checkResponseCode(t, http.StatusOK, rr.Code) + err := render.DecodeJSON(rr.Body, &user) + if err != nil { + t.Errorf("Error get user: %v", err) + } + user.MaxSessions = 1 + user.QuotaFiles = 2 + user.QuotaSize = 3 + user.GID = 1000 + form := make(url.Values) + form.Set("username", user.Username) + form.Set("home_dir", user.HomeDir) + form.Set("uid", "0") + form.Set("gid", strconv.FormatInt(int64(user.GID), 10)) + form.Set("max_sessions", strconv.FormatInt(int64(user.MaxSessions), 10)) + form.Set("quota_size", strconv.FormatInt(user.QuotaSize, 10)) + form.Set("quota_files", strconv.FormatInt(int64(user.QuotaFiles), 10)) + form.Set("upload_bandwidth", "0") + form.Set("download_bandwidth", "0") + form.Set("permissions", "*") + req, _ = http.NewRequest(http.MethodPost, webUserPath+"/"+strconv.FormatInt(user.ID, 10), strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + rr = executeRequest(req) + checkResponseCode(t, http.StatusSeeOther, rr.Code) + req, _ = http.NewRequest(http.MethodGet, userPath+"?limit=1&offset=0&order=ASC&username="+user.Username, nil) + rr = executeRequest(req) + checkResponseCode(t, http.StatusOK, rr.Code) + var users []dataprovider.User + err = render.DecodeJSON(rr.Body, &users) + if err != nil { + t.Errorf("Error decoding users: %v", err) + } + if len(users) != 1 { + t.Errorf("1 user is expected") + } + updateUser := users[0] + if user.HomeDir != updateUser.HomeDir { + t.Errorf("home dir does not match") + } + if user.MaxSessions != updateUser.MaxSessions { + t.Errorf("max_sessions does not match") + } + if user.QuotaFiles != updateUser.QuotaFiles { + t.Errorf("quota_files does not match") + } + if user.QuotaSize != updateUser.QuotaSize { + t.Errorf("quota_size does not match") + } + if user.GID != updateUser.GID { + t.Errorf("gid does not match") + } + req, _ = http.NewRequest(http.MethodDelete, userPath+"/"+strconv.FormatInt(user.ID, 10), nil) + rr = executeRequest(req) + checkResponseCode(t, http.StatusOK, rr.Code) +} + +func TestProviderClosedMock(t *testing.T) { + if providerDriverName == dataprovider.BoltDataProviderName { + t.Skip("skipping test provider errors for bolt provider") + } + dataProvider := dataprovider.GetProvider() + dataprovider.Close(dataProvider) + req, _ := http.NewRequest(http.MethodGet, webUsersPath, nil) + rr := executeRequest(req) + checkResponseCode(t, http.StatusInternalServerError, rr.Code) + req, _ = http.NewRequest(http.MethodGet, webUserPath+"/0", nil) + rr = executeRequest(req) + checkResponseCode(t, http.StatusInternalServerError, rr.Code) + form := make(url.Values) + form.Set("username", "test") + req, _ = http.NewRequest(http.MethodPost, webUserPath+"/0", strings.NewReader(form.Encode())) + rr = executeRequest(req) + checkResponseCode(t, http.StatusInternalServerError, rr.Code) + config.LoadConfig(configDir, "") + providerConf := config.GetProviderConf() + err := dataprovider.Initialize(providerConf, configDir) + if err != nil { + t.Errorf("error initializing data provider") + } + httpd.SetDataProvider(dataprovider.GetProvider()) + sftpd.SetDataProvider(dataprovider.GetProvider()) +} + +func TestGetWebConnectionsMock(t *testing.T) { + req, _ := http.NewRequest(http.MethodGet, webConnectionsPath, nil) + rr := executeRequest(req) + checkResponseCode(t, http.StatusOK, rr.Code) +} + +func TestStaticFilesMock(t *testing.T) { + req, _ := http.NewRequest(http.MethodGet, "/static/favicon.ico", nil) + rr := executeRequest(req) + checkResponseCode(t, http.StatusOK, rr.Code) +} + func waitTCPListening(address string) { for { conn, err := net.Dial("tcp", address) diff --git a/api/internal_test.go b/httpd/internal_test.go similarity index 93% rename from api/internal_test.go rename to httpd/internal_test.go index 02c3d70d..9e7d5628 100644 --- a/api/internal_test.go +++ b/httpd/internal_test.go @@ -1,4 +1,4 @@ -package api +package httpd import ( "context" @@ -6,6 +6,7 @@ import ( "net/http" "net/http/httptest" "testing" + "text/template" "github.com/drakkan/sftpgo/dataprovider" "github.com/go-chi/chi" @@ -226,3 +227,17 @@ func TestCloseConnectionHandler(t *testing.T) { t.Errorf("Expected response code 400. Got %d", rr.Code) } } + +func TestRenderInvalidTemplate(t *testing.T) { + tmpl, err := template.New("test").Parse("{{.Count}}") + if err != nil { + t.Errorf("error making test template: %v", err) + } else { + templates["no_match"] = tmpl + rw := httptest.NewRecorder() + renderTemplate(rw, "no_match", map[string]string{}) + if rw.Code != http.StatusInternalServerError { + t.Errorf("invalid template rendering must fail") + } + } +} diff --git a/api/router.go b/httpd/router.go similarity index 60% rename from api/router.go rename to httpd/router.go index 584da461..448f1e81 100644 --- a/api/router.go +++ b/httpd/router.go @@ -1,4 +1,4 @@ -package api +package httpd import ( "net/http" @@ -17,7 +17,7 @@ func GetHTTPRouter() http.Handler { return router } -func initializeRouter() { +func initializeRouter(staticFilesPath string) { router = chi.NewRouter() router.Use(middleware.RequestID) router.Use(middleware.RealIP) @@ -32,6 +32,14 @@ func initializeRouter() { sendAPIResponse(w, r, nil, "Method not allowed", http.StatusMethodNotAllowed) })) + router.Get("/", func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, webUsersPath, http.StatusMovedPermanently) + }) + + router.Get(webBasePath, func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, webUsersPath, http.StatusMovedPermanently) + }) + router.Handle(metricsPath, promhttp.Handler()) router.Get(versionPath, func(w http.ResponseWriter, r *http.Request) { @@ -73,6 +81,35 @@ func initializeRouter() { router.Delete(userPath+"/{userID}", func(w http.ResponseWriter, r *http.Request) { deleteUser(w, r) }) + + router.Get(webUsersPath, func(w http.ResponseWriter, r *http.Request) { + handleGetWebUsers(w, r) + }) + + router.Get(webUserPath, func(w http.ResponseWriter, r *http.Request) { + handleWebAddUserGet(w, r) + }) + + router.Get(webUserPath+"/{userID}", func(w http.ResponseWriter, r *http.Request) { + handleWebUpdateUserGet(chi.URLParam(r, "userID"), w, r) + }) + + router.Post(webUserPath, func(w http.ResponseWriter, r *http.Request) { + handleWebAddUserPost(w, r) + }) + + router.Post(webUserPath+"/{userID}", func(w http.ResponseWriter, r *http.Request) { + handleWebUpdateUserPost(chi.URLParam(r, "userID"), w, r) + }) + + router.Get(webConnectionsPath, func(w http.ResponseWriter, r *http.Request) { + handleWebGetConnections(w, r) + }) + + router.Group(func(router chi.Router) { + router.Use(middleware.DefaultCompress) + fileServer(router, staticFileWebPath, http.Dir(staticFilesPath)) + }) } func handleCloseConnection(w http.ResponseWriter, r *http.Request) { @@ -87,3 +124,17 @@ func handleCloseConnection(w http.ResponseWriter, r *http.Request) { sendAPIResponse(w, r, nil, "Not Found", http.StatusNotFound) } } + +func fileServer(r chi.Router, path string, root http.FileSystem) { + fs := http.StripPrefix(path, http.FileServer(root)) + + if path != "/" && path[len(path)-1] != '/' { + r.Get(path, http.RedirectHandler(path+"/", http.StatusMovedPermanently).ServeHTTP) + path += "/" + } + path += "*" + + r.Get(path, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fs.ServeHTTP(w, r) + })) +} diff --git a/api/schema/openapi.yaml b/httpd/schema/openapi.yaml similarity index 98% rename from api/schema/openapi.yaml rename to httpd/schema/openapi.yaml index 9c611f9e..611dec28 100644 --- a/api/schema/openapi.yaml +++ b/httpd/schema/openapi.yaml @@ -184,7 +184,7 @@ paths: tags: - users summary: Returns an array with one or more users - description: For security reasons passwords are empty in the response + description: For security reasons hashed passwords are omitted in the response operationId: get_users parameters: - in: query @@ -311,7 +311,7 @@ paths: tags: - users summary: Find user by ID - description: For security reasons passwords are empty in the response + description: For security reasons the hashed password is omitted in the response operationId: get_user_by_id parameters: - name: userID @@ -568,7 +568,7 @@ components: quota_size: type: integer format: int64 - description: quota as size. 0 menas unlimited. Please note that quota is updated if files are added/removed via SFTP/SCP otherwise a quota scan is needed + description: quota as size in bytes. 0 menas unlimited. Please note that quota is updated if files are added/removed via SFTP/SCP otherwise a quota scan is needed quota_files: type: integer format: int32 diff --git a/httpd/web.go b/httpd/web.go new file mode 100644 index 00000000..e6b59ad5 --- /dev/null +++ b/httpd/web.go @@ -0,0 +1,340 @@ +package httpd + +import ( + "fmt" + "net/http" + "path/filepath" + "strconv" + "strings" + "text/template" + + "github.com/drakkan/sftpgo/dataprovider" + "github.com/drakkan/sftpgo/sftpd" +) + +const ( + templateBase = "base.html" + templateUsers = "users.html" + templateUser = "user.html" + templateConnections = "connections.html" + templateMessage = "message.html" + + webBasePath = "/web" + webUsersPath = "/web/users" + webUserPath = "/web/user" + webConnectionsPath = "/web/connections" + staticFileWebPath = "/static" + + pageUsersTitle = "Users" + pageConnectionsTitle = "Connections" + page400Title = "Bad request" + page404Title = "Not found" + page404Body = "The page you are looking for does not exist." + page500Title = "Internal Server Error" + page500Body = "The server is unable to fulfill your request." + defaultUsersQueryLimit = 500 +) + +var ( + templates = make(map[string]*template.Template) +) + +type basePage struct { + Title string + CurrentURL string + UsersURL string + UserURL string + APIUserURL string + APIConnectionsURL string + ConnectionsURL string + UsersTitle string + ConnectionsTitle string +} + +type usersPage struct { + basePage + Users []dataprovider.User +} + +type connectionsPage struct { + basePage + Connections []sftpd.ConnectionStatus +} + +type userPage struct { + basePage + IsAdd bool + User dataprovider.User + Error string + ValidPerms []string +} + +type messagePage struct { + basePage + Error string + Success string +} + +func loadTemplates(templatesPath string) { + usersPaths := []string{ + filepath.Join(templatesPath, templateBase), + filepath.Join(templatesPath, templateUsers), + } + userPaths := []string{ + filepath.Join(templatesPath, templateBase), + filepath.Join(templatesPath, templateUser), + } + connectionsPaths := []string{ + filepath.Join(templatesPath, templateBase), + filepath.Join(templatesPath, templateConnections), + } + messagePath := []string{ + filepath.Join(templatesPath, templateBase), + filepath.Join(templatesPath, templateMessage), + } + usersTmpl := template.Must(template.ParseFiles(usersPaths...)) + userTmpl := template.Must(template.ParseFiles(userPaths...)) + connectionsTmpl := template.Must(template.ParseFiles(connectionsPaths...)) + messageTmpl := template.Must(template.ParseFiles(messagePath...)) + + templates[templateUsers] = usersTmpl + templates[templateUser] = userTmpl + templates[templateConnections] = connectionsTmpl + templates[templateMessage] = messageTmpl +} + +func getBasePageData(title, currentURL string) basePage { + return basePage{ + Title: title, + CurrentURL: currentURL, + UsersURL: webUsersPath, + UserURL: webUserPath, + APIUserURL: userPath, + APIConnectionsURL: activeConnectionsPath, + ConnectionsURL: webConnectionsPath, + UsersTitle: pageUsersTitle, + ConnectionsTitle: pageConnectionsTitle, + } +} + +func renderTemplate(w http.ResponseWriter, tmplName string, data interface{}) { + err := templates[tmplName].ExecuteTemplate(w, tmplName, data) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +func renderMessagePage(w http.ResponseWriter, title, body string, statusCode int, err error, message string) { + var errorString string + if len(body) > 0 { + errorString = body + " " + } + if err != nil { + errorString += err.Error() + } + data := messagePage{ + basePage: getBasePageData(title, ""), + Error: errorString, + Success: message, + } + w.WriteHeader(statusCode) + renderTemplate(w, templateMessage, data) +} + +func renderInternalServerErrorPage(w http.ResponseWriter, err error) { + renderMessagePage(w, page500Title, page400Title, http.StatusInternalServerError, err, "") +} + +func renderBadRequestPage(w http.ResponseWriter, err error) { + renderMessagePage(w, page400Title, "", http.StatusBadRequest, err, "") +} + +func renderNotFoundPage(w http.ResponseWriter, err error) { + renderMessagePage(w, page404Title, page404Body, http.StatusNotFound, err, "") +} + +func renderAddUserPage(w http.ResponseWriter, user dataprovider.User, error string) { + data := userPage{ + basePage: getBasePageData("Add a new user", webUserPath), + IsAdd: true, + Error: error, + User: user, + ValidPerms: dataprovider.ValidPerms, + } + renderTemplate(w, templateUser, data) +} + +func renderUpdateUserPage(w http.ResponseWriter, user dataprovider.User, error string) { + data := userPage{ + basePage: getBasePageData("Update user", fmt.Sprintf("%v/%v", webUserPath, user.ID)), + IsAdd: false, + Error: error, + User: user, + ValidPerms: dataprovider.ValidPerms, + } + renderTemplate(w, templateUser, data) +} + +func getUserFromPostFields(r *http.Request) (dataprovider.User, error) { + var user dataprovider.User + err := r.ParseForm() + if err != nil { + return user, err + } + publicKeysFormValue := r.Form.Get("public_keys") + publicKeys := []string{} + for _, v := range strings.Split(publicKeysFormValue, "\n") { + cleaned := strings.TrimSpace(v) + if len(cleaned) > 0 { + publicKeys = append(publicKeys, cleaned) + } + } + uid, err := strconv.Atoi(r.Form.Get("uid")) + if err != nil { + return user, err + } + gid, err := strconv.Atoi(r.Form.Get("gid")) + if err != nil { + return user, err + } + maxSessions, err := strconv.Atoi(r.Form.Get("max_sessions")) + if err != nil { + return user, err + } + quotaSize, err := strconv.ParseInt(r.Form.Get("quota_size"), 10, 64) + if err != nil { + return user, err + } + quotaFiles, err := strconv.Atoi(r.Form.Get("quota_files")) + if err != nil { + return user, err + } + bandwidthUL, err := strconv.ParseInt(r.Form.Get("upload_bandwidth"), 10, 64) + if err != nil { + return user, err + } + bandwidthDL, err := strconv.ParseInt(r.Form.Get("download_bandwidth"), 10, 64) + if err != nil { + return user, err + } + user = dataprovider.User{ + Username: r.Form.Get("username"), + Password: r.Form.Get("password"), + PublicKeys: publicKeys, + HomeDir: r.Form.Get("home_dir"), + UID: uid, + GID: gid, + Permissions: r.Form["permissions"], + MaxSessions: maxSessions, + QuotaSize: quotaSize, + QuotaFiles: quotaFiles, + UploadBandwidth: bandwidthUL, + DownloadBandwidth: bandwidthDL, + } + return user, err +} + +func handleGetWebUsers(w http.ResponseWriter, r *http.Request) { + limit := defaultUsersQueryLimit + if _, ok := r.URL.Query()["qlimit"]; ok { + var err error + limit, err = strconv.Atoi(r.URL.Query().Get("qlimit")) + if err != nil { + limit = defaultUsersQueryLimit + } + } + var users []dataprovider.User + u, err := dataprovider.GetUsers(dataProvider, limit, 0, "ASC", "") + users = append(users, u...) + for len(u) == limit { + u, err = dataprovider.GetUsers(dataProvider, limit, len(users), "ASC", "") + if err == nil && len(u) > 0 { + users = append(users, u...) + } else { + break + } + } + if err != nil { + renderInternalServerErrorPage(w, err) + return + } + data := usersPage{ + basePage: getBasePageData(pageUsersTitle, webUsersPath), + Users: users, + } + renderTemplate(w, templateUsers, data) +} + +func handleWebAddUserGet(w http.ResponseWriter, r *http.Request) { + renderAddUserPage(w, dataprovider.User{}, "") +} + +func handleWebUpdateUserGet(userID string, w http.ResponseWriter, r *http.Request) { + id, err := strconv.ParseInt(userID, 10, 64) + if err != nil { + renderBadRequestPage(w, err) + return + } + user, err := dataprovider.GetUserByID(dataProvider, id) + if err == nil { + renderUpdateUserPage(w, user, "") + } else if _, ok := err.(*dataprovider.RecordNotFoundError); ok { + renderNotFoundPage(w, err) + } else { + renderInternalServerErrorPage(w, err) + } +} + +func handleWebAddUserPost(w http.ResponseWriter, r *http.Request) { + user, err := getUserFromPostFields(r) + if err != nil { + renderAddUserPage(w, user, err.Error()) + return + } + err = dataprovider.AddUser(dataProvider, user) + if err == nil { + http.Redirect(w, r, webUsersPath, http.StatusSeeOther) + } else { + renderAddUserPage(w, user, err.Error()) + } +} + +func handleWebUpdateUserPost(userID string, w http.ResponseWriter, r *http.Request) { + id, err := strconv.ParseInt(userID, 10, 64) + if err != nil { + renderBadRequestPage(w, err) + return + } + user, err := dataprovider.GetUserByID(dataProvider, id) + if _, ok := err.(*dataprovider.RecordNotFoundError); ok { + renderNotFoundPage(w, err) + return + } else if err != nil { + renderInternalServerErrorPage(w, err) + return + } + updatedUser, err := getUserFromPostFields(r) + if err != nil { + renderUpdateUserPage(w, user, err.Error()) + return + } + updatedUser.ID = user.ID + if len(updatedUser.Password) == 0 { + updatedUser.Password = user.Password + } + err = dataprovider.UpdateUser(dataProvider, updatedUser) + if err == nil { + http.Redirect(w, r, webUsersPath, http.StatusSeeOther) + } else { + renderUpdateUserPage(w, user, err.Error()) + } +} + +func handleWebGetConnections(w http.ResponseWriter, r *http.Request) { + connectionStats := sftpd.GetConnectionsStats() + data := connectionsPage{ + basePage: getBasePageData(pageConnectionsTitle, webConnectionsPath), + Connections: connectionStats, + } + renderTemplate(w, templateConnections, data) +} diff --git a/service/service.go b/service/service.go index 0c04b8be..8e73ffac 100644 --- a/service/service.go +++ b/service/service.go @@ -1,13 +1,9 @@ package service import ( - "fmt" - "net/http" - "time" - - "github.com/drakkan/sftpgo/api" "github.com/drakkan/sftpgo/config" "github.com/drakkan/sftpgo/dataprovider" + "github.com/drakkan/sftpgo/httpd" "github.com/drakkan/sftpgo/logger" "github.com/drakkan/sftpgo/sftpd" "github.com/rs/zerolog" @@ -66,19 +62,10 @@ func (s *Service) Start() error { }() if httpdConf.BindPort > 0 { - router := api.GetHTTPRouter() - api.SetDataProvider(dataProvider) + httpd.SetDataProvider(dataProvider) go func() { - logger.Debug(logSender, "", "initializing HTTP server with config %+v", httpdConf) - httpServer := &http.Server{ - Addr: fmt.Sprintf("%s:%d", httpdConf.BindAddress, httpdConf.BindPort), - Handler: router, - ReadTimeout: 300 * time.Second, - WriteTimeout: 300 * time.Second, - MaxHeaderBytes: 1 << 20, // 1MB - } - if err := httpServer.ListenAndServe(); err != nil { + if err := httpdConf.Initialize(s.ConfigDir); err != nil { logger.Error(logSender, "", "could not start HTTP server: %v", err) logger.ErrorToConsole("could not start HTTP server: %v", err) } diff --git a/sftpd/internal_test.go b/sftpd/internal_test.go index 46374c6a..bb9e6eea 100644 --- a/sftpd/internal_test.go +++ b/sftpd/internal_test.go @@ -11,6 +11,7 @@ import ( "time" "github.com/drakkan/sftpgo/dataprovider" + "github.com/drakkan/sftpgo/utils" "github.com/pkg/sftp" ) @@ -722,3 +723,45 @@ func TestUploadError(t *testing.T) { t.Errorf("file uploaded must be deleted after an error: %v", err) } } + +func TestConnectionStatusStruct(t *testing.T) { + var transfers []connectionTransfer + transferUL := connectionTransfer{ + OperationType: operationUpload, + StartTime: utils.GetTimeAsMsSinceEpoch(time.Now()), + Size: 123, + LastActivity: utils.GetTimeAsMsSinceEpoch(time.Now()), + Path: "/test.upload", + } + transferDL := connectionTransfer{ + OperationType: operationDownload, + StartTime: utils.GetTimeAsMsSinceEpoch(time.Now()), + Size: 123, + LastActivity: utils.GetTimeAsMsSinceEpoch(time.Now()), + Path: "/test.download", + } + transfers = append(transfers, transferUL) + transfers = append(transfers, transferDL) + c := ConnectionStatus{ + Username: "test", + ConnectionID: "123", + ClientVersion: "fakeClient-1.0.0", + RemoteAddress: "127.0.0.1:1234", + ConnectionTime: utils.GetTimeAsMsSinceEpoch(time.Now()), + LastActivity: utils.GetTimeAsMsSinceEpoch(time.Now()), + Protocol: "SFTP", + Transfers: transfers, + } + durationString := c.GetConnectionDuration() + if len(durationString) == 0 { + t.Errorf("error getting connection duration") + } + transfersString := c.GetTransfersAsString() + if len(transfersString) == 0 { + t.Errorf("error getting transfers as string") + } + connInfo := c.GetConnectionInfo() + if len(connInfo) == 0 { + t.Errorf("error getting connection info") + } +} diff --git a/sftpd/sftpd.go b/sftpd/sftpd.go index 7794f778..bbfd1828 100644 --- a/sftpd/sftpd.go +++ b/sftpd/sftpd.go @@ -101,6 +101,47 @@ func init() { idleConnectionTicker = time.NewTicker(5 * time.Minute) } +// GetConnectionDuration returns the connection duration as string +func (c ConnectionStatus) GetConnectionDuration() string { + elapsed := time.Since(utils.GetTimeFromMsecSinceEpoch(c.ConnectionTime)) + return utils.GetDurationAsString(elapsed) +} + +// GetConnectionInfo returns connection info. +// Protocol,Client Version and RemoteAddress are returned +func (c ConnectionStatus) GetConnectionInfo() string { + return fmt.Sprintf("%v. Client: %#v From: %#v", c.Protocol, c.ClientVersion, c.RemoteAddress) +} + +// GetTransfersAsString returns the active transfers as string +func (c ConnectionStatus) GetTransfersAsString() string { + result := "" + for _, t := range c.Transfers { + if len(result) > 0 { + result += ". " + } + result += t.getConnectionTransferAsString() + } + return result +} + +func (t connectionTransfer) getConnectionTransferAsString() string { + result := "" + if t.OperationType == operationUpload { + result += "UL" + } else { + result += "DL" + } + result += fmt.Sprintf(" %#v ", t.Path) + if t.Size > 0 { + elapsed := time.Since(utils.GetTimeFromMsecSinceEpoch(t.StartTime)) + speed := float64(t.Size) / float64(utils.GetTimeAsMsSinceEpoch(time.Now())-t.StartTime) + result += fmt.Sprintf("Size: %#v Elapsed: %#v Speed: \"%.1f KB/s\"", utils.ByteCountSI(t.Size), + utils.GetDurationAsString(elapsed), speed) + } + return result +} + // SetDataProvider sets the data provider to use to authenticate users and to get/update their disk quota func SetDataProvider(provider dataprovider.Provider) { dataProvider = provider diff --git a/sftpd/sftpd_test.go b/sftpd/sftpd_test.go index 2524277d..5d317947 100644 --- a/sftpd/sftpd_test.go +++ b/sftpd/sftpd_test.go @@ -21,9 +21,9 @@ import ( "golang.org/x/crypto/ssh" - "github.com/drakkan/sftpgo/api" "github.com/drakkan/sftpgo/config" "github.com/drakkan/sftpgo/dataprovider" + "github.com/drakkan/sftpgo/httpd" "github.com/drakkan/sftpgo/logger" "github.com/drakkan/sftpgo/sftpd" "github.com/pkg/sftp" @@ -103,7 +103,6 @@ func TestMain(m *testing.M) { dataProvider := dataprovider.GetProvider() sftpdConf := config.GetSFTPDConfig() httpdConf := config.GetHTTPDConfig() - router := api.GetHTTPRouter() sftpdConf.BindPort = 2022 sftpdConf.KexAlgorithms = []string{"curve25519-sha256@libssh.org", "ecdh-sha2-nistp256", "ecdh-sha2-nistp384"} @@ -137,7 +136,7 @@ func TestMain(m *testing.M) { } sftpd.SetDataProvider(dataProvider) - api.SetDataProvider(dataProvider) + httpd.SetDataProvider(dataProvider) scpPath, err = exec.LookPath("scp") if err != nil { @@ -154,15 +153,7 @@ func TestMain(m *testing.M) { }() go func() { - logger.Debug(logSender, "", "initializing HTTP server with config %+v", httpdConf) - s := &http.Server{ - Addr: fmt.Sprintf("%s:%d", httpdConf.BindAddress, httpdConf.BindPort), - Handler: router, - ReadTimeout: 300 * time.Second, - WriteTimeout: 300 * time.Second, - MaxHeaderBytes: 1 << 20, // 1MB - } - if err := s.ListenAndServe(); err != nil { + if err := httpdConf.Initialize(configDir); err != nil { logger.Error(logSender, "", "could not start HTTP server: %v", err) } }() @@ -192,7 +183,7 @@ func TestBasicSFTPHandling(t *testing.T) { usePubKey := false u := getTestUser(usePubKey) u.QuotaSize = 6553600 - user, _, err := api.AddUser(u, http.StatusOK) + user, _, err := httpd.AddUser(u, http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) } @@ -219,7 +210,7 @@ func TestBasicSFTPHandling(t *testing.T) { if err != nil { t.Errorf("file download error: %v", err) } - user, _, err = api.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) if err != nil { t.Errorf("error getting user: %v", err) } @@ -237,7 +228,7 @@ func TestBasicSFTPHandling(t *testing.T) { if err == nil { t.Errorf("stat for deleted file must not succeed") } - user, _, err = api.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) if err != nil { t.Errorf("error getting user: %v", err) } @@ -248,7 +239,7 @@ func TestBasicSFTPHandling(t *testing.T) { t.Errorf("quota size does not match, expected: %v, actual: %v", expectedQuotaSize-testFileSize, user.UsedQuotaSize) } } - _, err = api.RemoveUser(user, http.StatusOK) + _, err = httpd.RemoveUser(user, http.StatusOK) if err != nil { t.Errorf("unable to remove user: %v", err) } @@ -257,7 +248,7 @@ func TestBasicSFTPHandling(t *testing.T) { func TestDirCommands(t *testing.T) { usePubKey := false - user, _, err := api.AddUser(getTestUser(usePubKey), http.StatusOK) + user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) } @@ -318,7 +309,7 @@ func TestDirCommands(t *testing.T) { t.Errorf("remove missing path must fail") } } - _, err = api.RemoveUser(user, http.StatusOK) + _, err = httpd.RemoveUser(user, http.StatusOK) if err != nil { t.Errorf("unable to remove user: %v", err) } @@ -327,7 +318,7 @@ func TestDirCommands(t *testing.T) { func TestLink(t *testing.T) { usePubKey := false - user, _, err := api.AddUser(getTestUser(usePubKey), http.StatusOK) + user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) } @@ -372,7 +363,7 @@ func TestLink(t *testing.T) { t.Errorf("error removing uploaded file: %v", err) } } - _, err = api.RemoveUser(user, http.StatusOK) + _, err = httpd.RemoveUser(user, http.StatusOK) if err != nil { t.Errorf("unable to remove user: %v", err) } @@ -381,7 +372,7 @@ func TestLink(t *testing.T) { func TestStat(t *testing.T) { usePubKey := false - user, _, err := api.AddUser(getTestUser(usePubKey), http.StatusOK) + user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) } @@ -429,7 +420,7 @@ func TestStat(t *testing.T) { t.Errorf("error removing uploaded file: %v", err) } } - _, err = api.RemoveUser(user, http.StatusOK) + _, err = httpd.RemoveUser(user, http.StatusOK) if err != nil { t.Errorf("unable to remove user: %v", err) } @@ -439,7 +430,7 @@ func TestStat(t *testing.T) { // basic tests to verify virtual chroot, should be improved to cover more cases ... func TestEscapeHomeDir(t *testing.T) { usePubKey := true - user, _, err := api.AddUser(getTestUser(usePubKey), http.StatusOK) + user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) } @@ -502,7 +493,7 @@ func TestEscapeHomeDir(t *testing.T) { } os.Remove(linkPath) } - _, err = api.RemoveUser(user, http.StatusOK) + _, err = httpd.RemoveUser(user, http.StatusOK) if err != nil { t.Errorf("unable to remove user: %v", err) } @@ -513,7 +504,7 @@ func TestHomeSpecialChars(t *testing.T) { usePubKey := true u := getTestUser(usePubKey) u.HomeDir = filepath.Join(homeBasePath, "abc açà#&%lk") - user, _, err := api.AddUser(u, http.StatusOK) + user, _, err := httpd.AddUser(u, http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) } @@ -549,7 +540,7 @@ func TestHomeSpecialChars(t *testing.T) { t.Errorf("error removing uploaded file: %v", err) } } - _, err = api.RemoveUser(user, http.StatusOK) + _, err = httpd.RemoveUser(user, http.StatusOK) if err != nil { t.Errorf("unable to remove user: %v", err) } @@ -559,7 +550,7 @@ func TestHomeSpecialChars(t *testing.T) { func TestLogin(t *testing.T) { u := getTestUser(false) u.PublicKeys = []string{testPubKey} - user, _, err := api.AddUser(u, http.StatusOK) + user, _, err := httpd.AddUser(u, http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) } @@ -592,7 +583,7 @@ func TestLogin(t *testing.T) { // testPubKey1 is not authorized user.PublicKeys = []string{testPubKey1} user.Password = "" - _, _, err = api.UpdateUser(user, http.StatusOK) + _, _, err = httpd.UpdateUser(user, http.StatusOK) if err != nil { t.Errorf("unable to update user: %v", err) } @@ -604,7 +595,7 @@ func TestLogin(t *testing.T) { // login a user with multiple public keys, only the second one is valid user.PublicKeys = []string{testPubKey1, testPubKey} user.Password = "" - _, _, err = api.UpdateUser(user, http.StatusOK) + _, _, err = httpd.UpdateUser(user, http.StatusOK) if err != nil { t.Errorf("unable to update user: %v", err) } @@ -618,7 +609,7 @@ func TestLogin(t *testing.T) { t.Errorf("sftp client with multiple public key must work if at least one public key is valid") } } - _, err = api.RemoveUser(user, http.StatusOK) + _, err = httpd.RemoveUser(user, http.StatusOK) if err != nil { t.Errorf("unable to remove user: %v", err) } @@ -627,14 +618,14 @@ func TestLogin(t *testing.T) { func TestLoginAfterUserUpdateEmptyPwd(t *testing.T) { usePubKey := false - user, _, err := api.AddUser(getTestUser(usePubKey), http.StatusOK) + user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) } user.Password = "" user.PublicKeys = []string{} // password and public key should remain unchanged - _, _, err = api.UpdateUser(user, http.StatusOK) + _, _, err = httpd.UpdateUser(user, http.StatusOK) if err != nil { t.Errorf("unable to update user: %v", err) } @@ -652,7 +643,7 @@ func TestLoginAfterUserUpdateEmptyPwd(t *testing.T) { t.Errorf("unable to read remote dir: %v", err) } } - _, err = api.RemoveUser(user, http.StatusOK) + _, err = httpd.RemoveUser(user, http.StatusOK) if err != nil { t.Errorf("unable to remove user: %v", err) } @@ -661,14 +652,14 @@ func TestLoginAfterUserUpdateEmptyPwd(t *testing.T) { func TestLoginAfterUserUpdateEmptyPubKey(t *testing.T) { usePubKey := true - user, _, err := api.AddUser(getTestUser(usePubKey), http.StatusOK) + user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) } user.Password = "" user.PublicKeys = []string{} // password and public key should remain unchanged - _, _, err = api.UpdateUser(user, http.StatusOK) + _, _, err = httpd.UpdateUser(user, http.StatusOK) if err != nil { t.Errorf("unable to update user: %v", err) } @@ -686,7 +677,7 @@ func TestLoginAfterUserUpdateEmptyPubKey(t *testing.T) { t.Errorf("unable to read remote dir: %v", err) } } - _, err = api.RemoveUser(user, http.StatusOK) + _, err = httpd.RemoveUser(user, http.StatusOK) if err != nil { t.Errorf("unable to remove user: %v", err) } @@ -697,7 +688,7 @@ func TestMaxSessions(t *testing.T) { usePubKey := false u := getTestUser(usePubKey) u.MaxSessions = 1 - user, _, err := api.AddUser(u, http.StatusOK) + user, _, err := httpd.AddUser(u, http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) } @@ -719,7 +710,7 @@ func TestMaxSessions(t *testing.T) { t.Errorf("max sessions exceeded, new login should not succeed") } } - _, err = api.RemoveUser(user, http.StatusOK) + _, err = httpd.RemoveUser(user, http.StatusOK) if err != nil { t.Errorf("unable to remove user: %v", err) } @@ -730,10 +721,11 @@ func TestQuotaFileReplace(t *testing.T) { usePubKey := false u := getTestUser(usePubKey) u.QuotaFiles = 1000 - user, _, err := api.AddUser(u, http.StatusOK) + user, _, err := httpd.AddUser(u, http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) } + os.RemoveAll(user.GetHomeDir()) testFileSize := int64(65535) testFileName := "test_file.dat" testFilePath := filepath.Join(homeBasePath, testFileName) @@ -752,7 +744,7 @@ func TestQuotaFileReplace(t *testing.T) { if err != nil { t.Errorf("file upload error: %v", err) } - user, _, err = api.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) if err != nil { t.Errorf("error getting user: %v", err) } @@ -761,7 +753,7 @@ func TestQuotaFileReplace(t *testing.T) { if err != nil { t.Errorf("file upload error: %v", err) } - user, _, err = api.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) if err != nil { t.Errorf("error getting user: %v", err) } @@ -774,7 +766,7 @@ func TestQuotaFileReplace(t *testing.T) { } // now set a quota size restriction and upload the same fail, upload should fail for space limit exceeded user.QuotaSize = testFileSize - 1 - user, _, err = api.UpdateUser(user, http.StatusOK) + user, _, err = httpd.UpdateUser(user, http.StatusOK) if err != nil { t.Errorf("error updating user: %v", err) } @@ -791,7 +783,7 @@ func TestQuotaFileReplace(t *testing.T) { t.Errorf("error removing uploaded file: %v", err) } } - _, err = api.RemoveUser(user, http.StatusOK) + _, err = httpd.RemoveUser(user, http.StatusOK) if err != nil { t.Errorf("unable to remove user: %v", err) } @@ -800,7 +792,7 @@ func TestQuotaFileReplace(t *testing.T) { func TestQuotaScan(t *testing.T) { usePubKey := false - user, _, err := api.AddUser(getTestUser(usePubKey), http.StatusOK) + user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) } @@ -823,31 +815,31 @@ func TestQuotaScan(t *testing.T) { t.Errorf("file upload error: %v", err) } } - _, err = api.RemoveUser(user, http.StatusOK) + _, err = httpd.RemoveUser(user, http.StatusOK) if err != nil { t.Errorf("unable to remove user: %v", err) } // create user with the same home dir, so there is at least an untracked file - user, _, err = api.AddUser(getTestUser(usePubKey), http.StatusOK) + user, _, err = httpd.AddUser(getTestUser(usePubKey), http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) } - _, err = api.StartQuotaScan(user, http.StatusCreated) + _, err = httpd.StartQuotaScan(user, http.StatusCreated) if err != nil { t.Errorf("error starting quota scan: %v", err) } - scans, _, err := api.GetQuotaScans(http.StatusOK) + scans, _, err := httpd.GetQuotaScans(http.StatusOK) if err != nil { t.Errorf("error getting active quota scans: %v", err) } for len(scans) > 0 { - scans, _, err = api.GetQuotaScans(http.StatusOK) + scans, _, err = httpd.GetQuotaScans(http.StatusOK) if err != nil { t.Errorf("error getting active quota scans: %v", err) break } } - user, _, err = api.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) if err != nil { t.Errorf("error getting user: %v", err) } @@ -857,7 +849,7 @@ func TestQuotaScan(t *testing.T) { if expectedQuotaSize != user.UsedQuotaSize { t.Errorf("quota size does not match after scan, expected: %v, actual: %v", expectedQuotaSize, user.UsedQuotaSize) } - _, err = api.RemoveUser(user, http.StatusOK) + _, err = httpd.RemoveUser(user, http.StatusOK) if err != nil { t.Errorf("unable to remove user: %v", err) } @@ -880,7 +872,7 @@ func TestQuotaSize(t *testing.T) { u := getTestUser(usePubKey) u.QuotaFiles = 1 u.QuotaSize = testFileSize - 1 - user, _, err := api.AddUser(u, http.StatusOK) + user, _, err := httpd.AddUser(u, http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) } @@ -908,7 +900,7 @@ func TestQuotaSize(t *testing.T) { t.Errorf("error removing uploaded file: %v", err) } } - _, err = api.RemoveUser(user, http.StatusOK) + _, err = httpd.RemoveUser(user, http.StatusOK) if err != nil { t.Errorf("unable to remove user: %v", err) } @@ -926,7 +918,7 @@ func TestBandwidthAndConnections(t *testing.T) { // 100 ms tolerance wantedUploadElapsed -= 100 wantedDownloadElapsed -= 100 - user, _, err := api.AddUser(u, http.StatusOK) + user, _, err := httpd.AddUser(u, http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) } @@ -980,7 +972,7 @@ func TestBandwidthAndConnections(t *testing.T) { t.Errorf("connection closed upload must fail") } } - _, err = api.RemoveUser(user, http.StatusOK) + _, err = httpd.RemoveUser(user, http.StatusOK) if err != nil { t.Errorf("unable to remove user: %v", err) } @@ -990,7 +982,7 @@ func TestBandwidthAndConnections(t *testing.T) { func TestMissingFile(t *testing.T) { usePubKey := false u := getTestUser(usePubKey) - user, _, err := api.AddUser(u, http.StatusOK) + user, _, err := httpd.AddUser(u, http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) } @@ -1005,7 +997,7 @@ func TestMissingFile(t *testing.T) { t.Errorf("download missing file must fail") } } - _, err = api.RemoveUser(user, http.StatusOK) + _, err = httpd.RemoveUser(user, http.StatusOK) if err != nil { t.Errorf("unable to remove user: %v", err) } @@ -1015,7 +1007,7 @@ func TestMissingFile(t *testing.T) { func TestOverwriteDirWithFile(t *testing.T) { usePubKey := false u := getTestUser(usePubKey) - user, _, err := api.AddUser(u, http.StatusOK) + user, _, err := httpd.AddUser(u, http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) } @@ -1057,7 +1049,7 @@ func TestOverwriteDirWithFile(t *testing.T) { t.Errorf("error removing uploaded file: %v", err) } } - _, err = api.RemoveUser(user, http.StatusOK) + _, err = httpd.RemoveUser(user, http.StatusOK) if err != nil { t.Errorf("unable to remove user: %v", err) } @@ -1070,7 +1062,7 @@ func TestPasswordsHashPbkdf2Sha1(t *testing.T) { usePubKey := false u := getTestUser(usePubKey) u.Password = pbkdf2Pwd - user, _, err := api.AddUser(u, http.StatusOK) + user, _, err := httpd.AddUser(u, http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) } @@ -1090,7 +1082,7 @@ func TestPasswordsHashPbkdf2Sha1(t *testing.T) { if err == nil { t.Errorf("login with wrong password must fail") } - _, err = api.RemoveUser(user, http.StatusOK) + _, err = httpd.RemoveUser(user, http.StatusOK) if err != nil { t.Errorf("unable to remove user: %v", err) } @@ -1103,7 +1095,7 @@ func TestPasswordsHashPbkdf2Sha256(t *testing.T) { usePubKey := false u := getTestUser(usePubKey) u.Password = pbkdf2Pwd - user, _, err := api.AddUser(u, http.StatusOK) + user, _, err := httpd.AddUser(u, http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) } @@ -1123,7 +1115,7 @@ func TestPasswordsHashPbkdf2Sha256(t *testing.T) { if err == nil { t.Errorf("login with wrong password must fail") } - _, err = api.RemoveUser(user, http.StatusOK) + _, err = httpd.RemoveUser(user, http.StatusOK) if err != nil { t.Errorf("unable to remove user: %v", err) } @@ -1136,7 +1128,7 @@ func TestPasswordsHashPbkdf2Sha512(t *testing.T) { usePubKey := false u := getTestUser(usePubKey) u.Password = pbkdf2Pwd - user, _, err := api.AddUser(u, http.StatusOK) + user, _, err := httpd.AddUser(u, http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) } @@ -1156,7 +1148,7 @@ func TestPasswordsHashPbkdf2Sha512(t *testing.T) { if err == nil { t.Errorf("login with wrong password must fail") } - _, err = api.RemoveUser(user, http.StatusOK) + _, err = httpd.RemoveUser(user, http.StatusOK) if err != nil { t.Errorf("unable to remove user: %v", err) } @@ -1169,7 +1161,7 @@ func TestPasswordsHashBcrypt(t *testing.T) { usePubKey := false u := getTestUser(usePubKey) u.Password = bcryptPwd - user, _, err := api.AddUser(u, http.StatusOK) + user, _, err := httpd.AddUser(u, http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) } @@ -1189,7 +1181,7 @@ func TestPasswordsHashBcrypt(t *testing.T) { if err == nil { t.Errorf("login with wrong password must fail") } - _, err = api.RemoveUser(user, http.StatusOK) + _, err = httpd.RemoveUser(user, http.StatusOK) if err != nil { t.Errorf("unable to remove user: %v", err) } @@ -1202,7 +1194,7 @@ func TestPasswordsHashSHA512Crypt(t *testing.T) { usePubKey := false u := getTestUser(usePubKey) u.Password = sha512CryptPwd - user, _, err := api.AddUser(u, http.StatusOK) + user, _, err := httpd.AddUser(u, http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) } @@ -1222,7 +1214,7 @@ func TestPasswordsHashSHA512Crypt(t *testing.T) { if err == nil { t.Errorf("login with wrong password must fail") } - _, err = api.RemoveUser(user, http.StatusOK) + _, err = httpd.RemoveUser(user, http.StatusOK) if err != nil { t.Errorf("unable to remove user: %v", err) } @@ -1234,7 +1226,7 @@ func TestPermList(t *testing.T) { u := getTestUser(usePubKey) u.Permissions = []string{dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete, dataprovider.PermRename, dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite} - user, _, err := api.AddUser(u, http.StatusOK) + user, _, err := httpd.AddUser(u, http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) } @@ -1252,7 +1244,7 @@ func TestPermList(t *testing.T) { t.Errorf("stat remote file without permission should not succeed") } } - _, err = api.RemoveUser(user, http.StatusOK) + _, err = httpd.RemoveUser(user, http.StatusOK) if err != nil { t.Errorf("unable to remove user: %v", err) } @@ -1264,7 +1256,7 @@ func TestPermDownload(t *testing.T) { u := getTestUser(usePubKey) u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermUpload, dataprovider.PermDelete, dataprovider.PermRename, dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite} - user, _, err := api.AddUser(u, http.StatusOK) + user, _, err := httpd.AddUser(u, http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) } @@ -1294,7 +1286,7 @@ func TestPermDownload(t *testing.T) { t.Errorf("error removing uploaded file: %v", err) } } - _, err = api.RemoveUser(user, http.StatusOK) + _, err = httpd.RemoveUser(user, http.StatusOK) if err != nil { t.Errorf("unable to remove user: %v", err) } @@ -1306,7 +1298,7 @@ func TestPermUpload(t *testing.T) { u := getTestUser(usePubKey) u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermDelete, dataprovider.PermRename, dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite} - user, _, err := api.AddUser(u, http.StatusOK) + user, _, err := httpd.AddUser(u, http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) } @@ -1327,7 +1319,7 @@ func TestPermUpload(t *testing.T) { t.Errorf("file upload without permission should not succeed") } } - _, err = api.RemoveUser(user, http.StatusOK) + _, err = httpd.RemoveUser(user, http.StatusOK) if err != nil { t.Errorf("unable to remove user: %v", err) } @@ -1339,7 +1331,7 @@ func TestPermOverwrite(t *testing.T) { u := getTestUser(usePubKey) u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete, dataprovider.PermRename, dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks} - user, _, err := api.AddUser(u, http.StatusOK) + user, _, err := httpd.AddUser(u, http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) } @@ -1364,7 +1356,7 @@ func TestPermOverwrite(t *testing.T) { t.Errorf("file overwrite without permission should not succeed") } } - _, err = api.RemoveUser(user, http.StatusOK) + _, err = httpd.RemoveUser(user, http.StatusOK) if err != nil { t.Errorf("unable to remove user: %v", err) } @@ -1376,7 +1368,7 @@ func TestPermDelete(t *testing.T) { u := getTestUser(usePubKey) u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermRename, dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite} - user, _, err := api.AddUser(u, http.StatusOK) + user, _, err := httpd.AddUser(u, http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) } @@ -1401,7 +1393,7 @@ func TestPermDelete(t *testing.T) { t.Errorf("delete without permission should not succeed") } } - _, err = api.RemoveUser(user, http.StatusOK) + _, err = httpd.RemoveUser(user, http.StatusOK) if err != nil { t.Errorf("unable to remove user: %v", err) } @@ -1413,7 +1405,7 @@ func TestPermRename(t *testing.T) { u := getTestUser(usePubKey) u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete, dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite} - user, _, err := api.AddUser(u, http.StatusOK) + user, _, err := httpd.AddUser(u, http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) } @@ -1442,7 +1434,7 @@ func TestPermRename(t *testing.T) { t.Errorf("error removing uploaded file: %v", err) } } - _, err = api.RemoveUser(user, http.StatusOK) + _, err = httpd.RemoveUser(user, http.StatusOK) if err != nil { t.Errorf("unable to remove user: %v", err) } @@ -1454,7 +1446,7 @@ func TestPermCreateDirs(t *testing.T) { u := getTestUser(usePubKey) u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete, dataprovider.PermRename, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite} - user, _, err := api.AddUser(u, http.StatusOK) + user, _, err := httpd.AddUser(u, http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) } @@ -1479,7 +1471,7 @@ func TestPermCreateDirs(t *testing.T) { t.Errorf("mkdir without permission should not succeed") } } - _, err = api.RemoveUser(user, http.StatusOK) + _, err = httpd.RemoveUser(user, http.StatusOK) if err != nil { t.Errorf("unable to remove user: %v", err) } @@ -1491,7 +1483,7 @@ func TestPermSymlink(t *testing.T) { u := getTestUser(usePubKey) u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete, dataprovider.PermRename, dataprovider.PermCreateDirs, dataprovider.PermOverwrite} - user, _, err := api.AddUser(u, http.StatusOK) + user, _, err := httpd.AddUser(u, http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) } @@ -1520,7 +1512,7 @@ func TestPermSymlink(t *testing.T) { t.Errorf("error removing uploaded file: %v", err) } } - _, err = api.RemoveUser(user, http.StatusOK) + _, err = httpd.RemoveUser(user, http.StatusOK) if err != nil { t.Errorf("unable to remove user: %v", err) } @@ -1529,7 +1521,7 @@ func TestPermSymlink(t *testing.T) { func TestSSHConnection(t *testing.T) { usePubKey := false - user, _, err := api.AddUser(getTestUser(usePubKey), http.StatusOK) + user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) } @@ -1537,7 +1529,7 @@ func TestSSHConnection(t *testing.T) { if err == nil { t.Errorf("ssh connection must fail: %v", err) } - _, err = api.RemoveUser(user, http.StatusOK) + _, err = httpd.RemoveUser(user, http.StatusOK) if err != nil { t.Errorf("unable to remove user: %v", err) } @@ -1551,7 +1543,7 @@ func TestSCPBasicHandling(t *testing.T) { usePubKey := true u := getTestUser(usePubKey) u.QuotaSize = 6553600 - user, _, err := api.AddUser(u, http.StatusOK) + user, _, err := httpd.AddUser(u, http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) } @@ -1589,7 +1581,7 @@ func TestSCPBasicHandling(t *testing.T) { } } os.Remove(localPath) - user, _, err = api.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) if err != nil { t.Errorf("error getting user: %v", err) } @@ -1603,7 +1595,7 @@ func TestSCPBasicHandling(t *testing.T) { if err != nil { t.Errorf("error removing uploaded files") } - _, err = api.RemoveUser(user, http.StatusOK) + _, err = httpd.RemoveUser(user, http.StatusOK) if err != nil { t.Errorf("unable to remove user: %v", err) } @@ -1616,7 +1608,7 @@ func TestSCPUploadFileOverwrite(t *testing.T) { usePubKey := true u := getTestUser(usePubKey) u.QuotaFiles = 1000 - user, _, err := api.AddUser(u, http.StatusOK) + user, _, err := httpd.AddUser(u, http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) } @@ -1638,7 +1630,7 @@ func TestSCPUploadFileOverwrite(t *testing.T) { if err != nil { t.Errorf("error uploading existing file via scp: %v", err) } - user, _, err = api.GetUserByID(user.ID, http.StatusOK) + user, _, err = httpd.GetUserByID(user.ID, http.StatusOK) if err != nil { t.Errorf("error getting user: %v", err) } @@ -1665,7 +1657,7 @@ func TestSCPUploadFileOverwrite(t *testing.T) { if err != nil { t.Errorf("error removing uploaded files") } - _, err = api.RemoveUser(user, http.StatusOK) + _, err = httpd.RemoveUser(user, http.StatusOK) if err != nil { t.Errorf("unable to remove user: %v", err) } @@ -1677,7 +1669,7 @@ func TestSCPRecursive(t *testing.T) { } usePubKey := true u := getTestUser(usePubKey) - user, _, err := api.AddUser(u, http.StatusOK) + user, _, err := httpd.AddUser(u, http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) } @@ -1739,7 +1731,7 @@ func TestSCPRecursive(t *testing.T) { if err != nil { t.Errorf("error removing uploaded files") } - _, err = api.RemoveUser(user, http.StatusOK) + _, err = httpd.RemoveUser(user, http.StatusOK) if err != nil { t.Errorf("unable to remove user: %v", err) } @@ -1752,7 +1744,7 @@ func TestSCPPermCreateDirs(t *testing.T) { usePubKey := true u := getTestUser(usePubKey) u.Permissions = []string{dataprovider.PermDownload, dataprovider.PermUpload} - user, _, err := api.AddUser(u, http.StatusOK) + user, _, err := httpd.AddUser(u, http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) } @@ -1788,7 +1780,7 @@ func TestSCPPermCreateDirs(t *testing.T) { if err != nil { t.Errorf("error removing uploaded files") } - _, err = api.RemoveUser(user, http.StatusOK) + _, err = httpd.RemoveUser(user, http.StatusOK) if err != nil { t.Errorf("unable to remove user: %v", err) } @@ -1801,7 +1793,7 @@ func TestSCPPermUpload(t *testing.T) { usePubKey := true u := getTestUser(usePubKey) u.Permissions = []string{dataprovider.PermDownload, dataprovider.PermCreateDirs} - user, _, err := api.AddUser(u, http.StatusOK) + user, _, err := httpd.AddUser(u, http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) } @@ -1825,7 +1817,7 @@ func TestSCPPermUpload(t *testing.T) { if err != nil { t.Errorf("error removing uploaded files") } - _, err = api.RemoveUser(user, http.StatusOK) + _, err = httpd.RemoveUser(user, http.StatusOK) if err != nil { t.Errorf("unable to remove user: %v", err) } @@ -1838,7 +1830,7 @@ func TestSCPPermOverwrite(t *testing.T) { usePubKey := true u := getTestUser(usePubKey) u.Permissions = []string{dataprovider.PermUpload, dataprovider.PermCreateDirs} - user, _, err := api.AddUser(u, http.StatusOK) + user, _, err := httpd.AddUser(u, http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) } @@ -1866,7 +1858,7 @@ func TestSCPPermOverwrite(t *testing.T) { if err != nil { t.Errorf("error removing uploaded files") } - _, err = api.RemoveUser(user, http.StatusOK) + _, err = httpd.RemoveUser(user, http.StatusOK) if err != nil { t.Errorf("unable to remove user: %v", err) } @@ -1879,7 +1871,7 @@ func TestSCPPermDownload(t *testing.T) { usePubKey := true u := getTestUser(usePubKey) u.Permissions = []string{dataprovider.PermUpload, dataprovider.PermCreateDirs} - user, _, err := api.AddUser(u, http.StatusOK) + user, _, err := httpd.AddUser(u, http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) } @@ -1909,7 +1901,7 @@ func TestSCPPermDownload(t *testing.T) { if err != nil { t.Errorf("error removing uploaded files") } - _, err = api.RemoveUser(user, http.StatusOK) + _, err = httpd.RemoveUser(user, http.StatusOK) if err != nil { t.Errorf("unable to remove user: %v", err) } @@ -1924,7 +1916,7 @@ func TestSCPQuotaSize(t *testing.T) { u := getTestUser(usePubKey) u.QuotaFiles = 1 u.QuotaSize = testFileSize - 1 - user, _, err := api.AddUser(u, http.StatusOK) + user, _, err := httpd.AddUser(u, http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) } @@ -1951,7 +1943,7 @@ func TestSCPQuotaSize(t *testing.T) { if err != nil { t.Errorf("error removing uploaded files") } - _, err = api.RemoveUser(user, http.StatusOK) + _, err = httpd.RemoveUser(user, http.StatusOK) if err != nil { t.Errorf("unable to remove user: %v", err) } @@ -1962,7 +1954,7 @@ func TestSCPEscapeHomeDir(t *testing.T) { t.Skip("scp command not found, unable to execute this test") } usePubKey := true - user, _, err := api.AddUser(getTestUser(usePubKey), http.StatusOK) + user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) } @@ -2004,7 +1996,7 @@ func TestSCPEscapeHomeDir(t *testing.T) { if err != nil { t.Errorf("error removing uploaded files") } - _, err = api.RemoveUser(user, http.StatusOK) + _, err = httpd.RemoveUser(user, http.StatusOK) if err != nil { t.Errorf("unable to remove user: %v", err) } @@ -2015,7 +2007,7 @@ func TestSCPUploadPaths(t *testing.T) { t.Skip("scp command not found, unable to execute this test") } usePubKey := true - user, _, err := api.AddUser(getTestUser(usePubKey), http.StatusOK) + user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) } @@ -2050,7 +2042,7 @@ func TestSCPUploadPaths(t *testing.T) { if err != nil { t.Errorf("error removing uploaded files") } - _, err = api.RemoveUser(user, http.StatusOK) + _, err = httpd.RemoveUser(user, http.StatusOK) if err != nil { t.Errorf("unable to remove user: %v", err) } @@ -2061,7 +2053,7 @@ func TestSCPOverwriteDirWithFile(t *testing.T) { t.Skip("scp command not found, unable to execute this test") } usePubKey := true - user, _, err := api.AddUser(getTestUser(usePubKey), http.StatusOK) + user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) } @@ -2083,7 +2075,7 @@ func TestSCPOverwriteDirWithFile(t *testing.T) { if err != nil { t.Errorf("error removing uploaded files") } - _, err = api.RemoveUser(user, http.StatusOK) + _, err = httpd.RemoveUser(user, http.StatusOK) if err != nil { t.Errorf("unable to remove user: %v", err) } @@ -2094,14 +2086,14 @@ func TestSCPRemoteToRemote(t *testing.T) { t.Skip("scp command not found, unable to execute this test") } usePubKey := true - user, _, err := api.AddUser(getTestUser(usePubKey), http.StatusOK) + user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) } u := getTestUser(usePubKey) u.Username += "1" u.HomeDir += "1" - user1, _, err := api.AddUser(u, http.StatusOK) + user1, _, err := httpd.AddUser(u, http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) } @@ -2126,7 +2118,7 @@ func TestSCPRemoteToRemote(t *testing.T) { if err != nil { t.Errorf("error removing uploaded files") } - _, err = api.RemoveUser(user, http.StatusOK) + _, err = httpd.RemoveUser(user, http.StatusOK) if err != nil { t.Errorf("unable to remove user: %v", err) } @@ -2134,7 +2126,7 @@ func TestSCPRemoteToRemote(t *testing.T) { if err != nil { t.Errorf("error removing uploaded files for user1") } - _, err = api.RemoveUser(user1, http.StatusOK) + _, err = httpd.RemoveUser(user1, http.StatusOK) if err != nil { t.Errorf("unable to remove user1: %v", err) } @@ -2145,7 +2137,7 @@ func TestSCPErrors(t *testing.T) { t.Skip("scp command not found, unable to execute this test") } u := getTestUser(true) - user, _, err := api.AddUser(u, http.StatusOK) + user, _, err := httpd.AddUser(u, http.StatusOK) if err != nil { t.Errorf("unable to add user: %v", err) } @@ -2165,7 +2157,7 @@ func TestSCPErrors(t *testing.T) { } user.UploadBandwidth = 512 user.DownloadBandwidth = 512 - _, _, err = api.UpdateUser(user, http.StatusOK) + _, _, err = httpd.UpdateUser(user, http.StatusOK) if err != nil { t.Errorf("unable to update user: %v", err) } @@ -2201,7 +2193,7 @@ func TestSCPErrors(t *testing.T) { if err != nil { t.Errorf("error removing uploaded files") } - _, err = api.RemoveUser(user, http.StatusOK) + _, err = httpd.RemoveUser(user, http.StatusOK) if err != nil { t.Errorf("unable to remove user: %v", err) } diff --git a/sftpgo.json b/sftpgo.json index 2d11ed9c..ea449601 100644 --- a/sftpgo.json +++ b/sftpgo.json @@ -36,6 +36,8 @@ }, "httpd": { "bind_port": 8080, - "bind_address": "127.0.0.1" + "bind_address": "127.0.0.1", + "templates_path": "templates", + "static_files_path": "static" } } diff --git a/static/css/fonts.css b/static/css/fonts.css new file mode 100644 index 00000000..fe4c8833 --- /dev/null +++ b/static/css/fonts.css @@ -0,0 +1,20 @@ +@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; + } \ No newline at end of file diff --git a/static/css/sb-admin-2.min.css b/static/css/sb-admin-2.min.css new file mode 100644 index 00000000..af2ffb3d --- /dev/null +++ b/static/css/sb-admin-2.min.css @@ -0,0 +1,10 @@ +/*! + * Start Bootstrap - SB Admin 2 v4.0.7 (https://startbootstrap.com/template-overviews/sb-admin-2) + * Copyright 2013-2019 Start Bootstrap + * Licensed under MIT (https://github.com/BlackrockDigital/startbootstrap-sb-admin-2/blob/master/LICENSE) + *//*! + * Bootstrap v4.3.1 (https://getbootstrap.com/) + * Copyright 2011-2019 The Bootstrap Authors + * Copyright 2011-2019 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */:root{--blue:#4e73df;--indigo:#6610f2;--purple:#6f42c1;--pink:#e83e8c;--red:#e74a3b;--orange:#fd7e14;--yellow:#f6c23e;--green:#1cc88a;--teal:#20c9a6;--cyan:#36b9cc;--white:#fff;--gray:#858796;--gray-dark:#5a5c69;--primary:#4e73df;--secondary:#858796;--success:#1cc88a;--info:#36b9cc;--warning:#f6c23e;--danger:#e74a3b;--light:#f8f9fc;--dark:#5a5c69;--breakpoint-xs:0;--breakpoint-sm:576px;--breakpoint-md:768px;--breakpoint-lg:992px;--breakpoint-xl:1200px;--font-family-sans-serif:"Nunito",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-family-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:Nunito,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#858796;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#4e73df;text-decoration:none;background-color:transparent}a:hover{color:#224abe;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#858796;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-bottom:.5rem;font-weight:400;line-height:1.2}.h1,h1{font-size:2.5rem}.h2,h2{font-size:2rem}.h3,h3{font-size:1.75rem}.h4,h4{font-size:1.5rem}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.2}.display-2{font-size:5.5rem;font-weight:300;line-height:1.2}.display-3{font-size:4.5rem;font-weight:300;line-height:1.2}.display-4{font-size:3.5rem;font-weight:300;line-height:1.2}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(0,0,0,.1)}.small,small{font-size:80%;font-weight:400}.mark,mark{padding:.2em;background-color:#fcf8e3}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:90%;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote-footer{display:block;font-size:80%;color:#858796}.blockquote-footer::before{content:"\2014\00A0"}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dddfeb;border-radius:.35rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:90%;color:#858796}code{font-size:87.5%;color:#e83e8c;word-break:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:87.5%;color:#fff;background-color:#3a3b45;border-radius:.2rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;font-size:87.5%;color:#3a3b45}pre code{font-size:inherit;color:inherit;word-break:normal}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{width:100%;padding-right:.75rem;padding-left:.75rem;margin-right:auto;margin-left:auto}@media (min-width:576px){.container{max-width:540px}}@media (min-width:768px){.container{max-width:720px}}@media (min-width:992px){.container{max-width:960px}}@media (min-width:1200px){.container{max-width:1140px}}.container-fluid{width:100%;padding-right:.75rem;padding-left:.75rem;margin-right:auto;margin-left:auto}.row{display:flex;flex-wrap:wrap;margin-right:-.75rem;margin-left:-.75rem}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-right:0;padding-left:0}.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-auto,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-auto,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-auto,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-auto,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-auto{position:relative;width:100%;padding-right:.75rem;padding-left:.75rem}.col{flex-basis:0;flex-grow:1;max-width:100%}.col-auto{flex:0 0 auto;width:auto;max-width:100%}.col-1{flex:0 0 8.33333%;max-width:8.33333%}.col-2{flex:0 0 16.66667%;max-width:16.66667%}.col-3{flex:0 0 25%;max-width:25%}.col-4{flex:0 0 33.33333%;max-width:33.33333%}.col-5{flex:0 0 41.66667%;max-width:41.66667%}.col-6{flex:0 0 50%;max-width:50%}.col-7{flex:0 0 58.33333%;max-width:58.33333%}.col-8{flex:0 0 66.66667%;max-width:66.66667%}.col-9{flex:0 0 75%;max-width:75%}.col-10{flex:0 0 83.33333%;max-width:83.33333%}.col-11{flex:0 0 91.66667%;max-width:91.66667%}.col-12{flex:0 0 100%;max-width:100%}.order-first{order:-1}.order-last{order:13}.order-0{order:0}.order-1{order:1}.order-2{order:2}.order-3{order:3}.order-4{order:4}.order-5{order:5}.order-6{order:6}.order-7{order:7}.order-8{order:8}.order-9{order:9}.order-10{order:10}.order-11{order:11}.order-12{order:12}.offset-1{margin-left:8.33333%}.offset-2{margin-left:16.66667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333%}.offset-5{margin-left:41.66667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333%}.offset-8{margin-left:66.66667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333%}.offset-11{margin-left:91.66667%}@media (min-width:576px){.col-sm{flex-basis:0;flex-grow:1;max-width:100%}.col-sm-auto{flex:0 0 auto;width:auto;max-width:100%}.col-sm-1{flex:0 0 8.33333%;max-width:8.33333%}.col-sm-2{flex:0 0 16.66667%;max-width:16.66667%}.col-sm-3{flex:0 0 25%;max-width:25%}.col-sm-4{flex:0 0 33.33333%;max-width:33.33333%}.col-sm-5{flex:0 0 41.66667%;max-width:41.66667%}.col-sm-6{flex:0 0 50%;max-width:50%}.col-sm-7{flex:0 0 58.33333%;max-width:58.33333%}.col-sm-8{flex:0 0 66.66667%;max-width:66.66667%}.col-sm-9{flex:0 0 75%;max-width:75%}.col-sm-10{flex:0 0 83.33333%;max-width:83.33333%}.col-sm-11{flex:0 0 91.66667%;max-width:91.66667%}.col-sm-12{flex:0 0 100%;max-width:100%}.order-sm-first{order:-1}.order-sm-last{order:13}.order-sm-0{order:0}.order-sm-1{order:1}.order-sm-2{order:2}.order-sm-3{order:3}.order-sm-4{order:4}.order-sm-5{order:5}.order-sm-6{order:6}.order-sm-7{order:7}.order-sm-8{order:8}.order-sm-9{order:9}.order-sm-10{order:10}.order-sm-11{order:11}.order-sm-12{order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333%}.offset-sm-2{margin-left:16.66667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333%}.offset-sm-5{margin-left:41.66667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333%}.offset-sm-8{margin-left:66.66667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333%}.offset-sm-11{margin-left:91.66667%}}@media (min-width:768px){.col-md{flex-basis:0;flex-grow:1;max-width:100%}.col-md-auto{flex:0 0 auto;width:auto;max-width:100%}.col-md-1{flex:0 0 8.33333%;max-width:8.33333%}.col-md-2{flex:0 0 16.66667%;max-width:16.66667%}.col-md-3{flex:0 0 25%;max-width:25%}.col-md-4{flex:0 0 33.33333%;max-width:33.33333%}.col-md-5{flex:0 0 41.66667%;max-width:41.66667%}.col-md-6{flex:0 0 50%;max-width:50%}.col-md-7{flex:0 0 58.33333%;max-width:58.33333%}.col-md-8{flex:0 0 66.66667%;max-width:66.66667%}.col-md-9{flex:0 0 75%;max-width:75%}.col-md-10{flex:0 0 83.33333%;max-width:83.33333%}.col-md-11{flex:0 0 91.66667%;max-width:91.66667%}.col-md-12{flex:0 0 100%;max-width:100%}.order-md-first{order:-1}.order-md-last{order:13}.order-md-0{order:0}.order-md-1{order:1}.order-md-2{order:2}.order-md-3{order:3}.order-md-4{order:4}.order-md-5{order:5}.order-md-6{order:6}.order-md-7{order:7}.order-md-8{order:8}.order-md-9{order:9}.order-md-10{order:10}.order-md-11{order:11}.order-md-12{order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333%}.offset-md-2{margin-left:16.66667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333%}.offset-md-5{margin-left:41.66667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333%}.offset-md-8{margin-left:66.66667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333%}.offset-md-11{margin-left:91.66667%}}@media (min-width:992px){.col-lg{flex-basis:0;flex-grow:1;max-width:100%}.col-lg-auto{flex:0 0 auto;width:auto;max-width:100%}.col-lg-1{flex:0 0 8.33333%;max-width:8.33333%}.col-lg-2{flex:0 0 16.66667%;max-width:16.66667%}.col-lg-3{flex:0 0 25%;max-width:25%}.col-lg-4{flex:0 0 33.33333%;max-width:33.33333%}.col-lg-5{flex:0 0 41.66667%;max-width:41.66667%}.col-lg-6{flex:0 0 50%;max-width:50%}.col-lg-7{flex:0 0 58.33333%;max-width:58.33333%}.col-lg-8{flex:0 0 66.66667%;max-width:66.66667%}.col-lg-9{flex:0 0 75%;max-width:75%}.col-lg-10{flex:0 0 83.33333%;max-width:83.33333%}.col-lg-11{flex:0 0 91.66667%;max-width:91.66667%}.col-lg-12{flex:0 0 100%;max-width:100%}.order-lg-first{order:-1}.order-lg-last{order:13}.order-lg-0{order:0}.order-lg-1{order:1}.order-lg-2{order:2}.order-lg-3{order:3}.order-lg-4{order:4}.order-lg-5{order:5}.order-lg-6{order:6}.order-lg-7{order:7}.order-lg-8{order:8}.order-lg-9{order:9}.order-lg-10{order:10}.order-lg-11{order:11}.order-lg-12{order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333%}.offset-lg-2{margin-left:16.66667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333%}.offset-lg-5{margin-left:41.66667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333%}.offset-lg-8{margin-left:66.66667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333%}.offset-lg-11{margin-left:91.66667%}}@media (min-width:1200px){.col-xl{flex-basis:0;flex-grow:1;max-width:100%}.col-xl-auto{flex:0 0 auto;width:auto;max-width:100%}.col-xl-1{flex:0 0 8.33333%;max-width:8.33333%}.col-xl-2{flex:0 0 16.66667%;max-width:16.66667%}.col-xl-3{flex:0 0 25%;max-width:25%}.col-xl-4{flex:0 0 33.33333%;max-width:33.33333%}.col-xl-5{flex:0 0 41.66667%;max-width:41.66667%}.col-xl-6{flex:0 0 50%;max-width:50%}.col-xl-7{flex:0 0 58.33333%;max-width:58.33333%}.col-xl-8{flex:0 0 66.66667%;max-width:66.66667%}.col-xl-9{flex:0 0 75%;max-width:75%}.col-xl-10{flex:0 0 83.33333%;max-width:83.33333%}.col-xl-11{flex:0 0 91.66667%;max-width:91.66667%}.col-xl-12{flex:0 0 100%;max-width:100%}.order-xl-first{order:-1}.order-xl-last{order:13}.order-xl-0{order:0}.order-xl-1{order:1}.order-xl-2{order:2}.order-xl-3{order:3}.order-xl-4{order:4}.order-xl-5{order:5}.order-xl-6{order:6}.order-xl-7{order:7}.order-xl-8{order:8}.order-xl-9{order:9}.order-xl-10{order:10}.order-xl-11{order:11}.order-xl-12{order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333%}.offset-xl-2{margin-left:16.66667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333%}.offset-xl-5{margin-left:41.66667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333%}.offset-xl-8{margin-left:66.66667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333%}.offset-xl-11{margin-left:91.66667%}}.table{width:100%;margin-bottom:1rem;color:#858796}.table td,.table th{padding:.75rem;vertical-align:top;border-top:1px solid #e3e6f0}.table thead th{vertical-align:bottom;border-bottom:2px solid #e3e6f0}.table tbody+tbody{border-top:2px solid #e3e6f0}.table-sm td,.table-sm th{padding:.3rem}.table-bordered{border:1px solid #e3e6f0}.table-bordered td,.table-bordered th{border:1px solid #e3e6f0}.table-bordered thead td,.table-bordered thead th{border-bottom-width:2px}.table-borderless tbody+tbody,.table-borderless td,.table-borderless th,.table-borderless thead th{border:0}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(0,0,0,.05)}.table-hover tbody tr:hover{color:#858796;background-color:rgba(0,0,0,.075)}.table-primary,.table-primary>td,.table-primary>th{background-color:#cdd8f6}.table-primary tbody+tbody,.table-primary td,.table-primary th,.table-primary thead th{border-color:#a3b6ee}.table-hover .table-primary:hover{background-color:#b7c7f2}.table-hover .table-primary:hover>td,.table-hover .table-primary:hover>th{background-color:#b7c7f2}.table-secondary,.table-secondary>td,.table-secondary>th{background-color:#dddde2}.table-secondary tbody+tbody,.table-secondary td,.table-secondary th,.table-secondary thead th{border-color:#c0c1c8}.table-hover .table-secondary:hover{background-color:#cfcfd6}.table-hover .table-secondary:hover>td,.table-hover .table-secondary:hover>th{background-color:#cfcfd6}.table-success,.table-success>td,.table-success>th{background-color:#bff0de}.table-success tbody+tbody,.table-success td,.table-success th,.table-success thead th{border-color:#89e2c2}.table-hover .table-success:hover{background-color:#aaebd3}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#aaebd3}.table-info,.table-info>td,.table-info>th{background-color:#c7ebf1}.table-info tbody+tbody,.table-info td,.table-info th,.table-info thead th{border-color:#96dbe4}.table-hover .table-info:hover{background-color:#b3e4ec}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#b3e4ec}.table-warning,.table-warning>td,.table-warning>th{background-color:#fceec9}.table-warning tbody+tbody,.table-warning td,.table-warning th,.table-warning thead th{border-color:#fadf9b}.table-hover .table-warning:hover{background-color:#fbe6b1}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#fbe6b1}.table-danger,.table-danger>td,.table-danger>th{background-color:#f8ccc8}.table-danger tbody+tbody,.table-danger td,.table-danger th,.table-danger thead th{border-color:#f3a199}.table-hover .table-danger:hover{background-color:#f5b7b1}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#f5b7b1}.table-light,.table-light>td,.table-light>th{background-color:#fdfdfe}.table-light tbody+tbody,.table-light td,.table-light th,.table-light thead th{border-color:#fbfcfd}.table-hover .table-light:hover{background-color:#ececf6}.table-hover .table-light:hover>td,.table-hover .table-light:hover>th{background-color:#ececf6}.table-dark,.table-dark>td,.table-dark>th{background-color:#d1d1d5}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#a9aab1}.table-hover .table-dark:hover{background-color:#c4c4c9}.table-hover .table-dark:hover>td,.table-hover .table-dark:hover>th{background-color:#c4c4c9}.table-active,.table-active>td,.table-active>th{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(0,0,0,.075)}.table .thead-dark th{color:#fff;background-color:#5a5c69;border-color:#6c6e7e}.table .thead-light th{color:#6e707e;background-color:#eaecf4;border-color:#e3e6f0}.table-dark{color:#fff;background-color:#5a5c69}.table-dark td,.table-dark th,.table-dark thead th{border-color:#6c6e7e}.table-dark.table-bordered{border:0}.table-dark.table-striped tbody tr:nth-of-type(odd){background-color:rgba(255,255,255,.05)}.table-dark.table-hover tbody tr:hover{color:#fff;background-color:rgba(255,255,255,.075)}@media (max-width:575.98px){.table-responsive-sm{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-sm>.table-bordered{border:0}}@media (max-width:767.98px){.table-responsive-md{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-md>.table-bordered{border:0}}@media (max-width:991.98px){.table-responsive-lg{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-lg>.table-bordered{border:0}}@media (max-width:1199.98px){.table-responsive-xl{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-xl>.table-bordered{border:0}}.table-responsive{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive>.table-bordered{border:0}.form-control{display:block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#6e707e;background-color:#fff;background-clip:padding-box;border:1px solid #d1d3e2;border-radius:.35rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:focus{color:#6e707e;background-color:#fff;border-color:#bac8f3;outline:0;box-shadow:0 0 0 .2rem rgba(78,115,223,.25)}.form-control::-webkit-input-placeholder{color:#858796;opacity:1}.form-control::-moz-placeholder{color:#858796;opacity:1}.form-control:-ms-input-placeholder{color:#858796;opacity:1}.form-control::-ms-input-placeholder{color:#858796;opacity:1}.form-control::placeholder{color:#858796;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#eaecf4;opacity:1}select.form-control:focus::-ms-value{color:#6e707e;background-color:#fff}.form-control-file,.form-control-range{display:block;width:100%}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem;line-height:1.5}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem;line-height:1.5}.form-control-plaintext{display:block;width:100%;padding-top:.375rem;padding-bottom:.375rem;margin-bottom:0;line-height:1.5;color:#858796;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.form-control-lg{height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}select.form-control[multiple],select.form-control[size]{height:auto}textarea.form-control{height:auto}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:.25rem}.form-row{display:flex;flex-wrap:wrap;margin-right:-5px;margin-left:-5px}.form-row>.col,.form-row>[class*=col-]{padding-right:5px;padding-left:5px}.form-check{position:relative;display:block;padding-left:1.25rem}.form-check-input{position:absolute;margin-top:.3rem;margin-left:-1.25rem}.form-check-input:disabled~.form-check-label{color:#858796}.form-check-label{margin-bottom:0}.form-check-inline{display:inline-flex;align-items:center;padding-left:0;margin-right:.75rem}.form-check-inline .form-check-input{position:static;margin-top:0;margin-right:.3125rem;margin-left:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#1cc88a}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(28,200,138,.9);border-radius:.35rem}.form-control.is-valid,.was-validated .form-control:valid{border-color:#1cc88a;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%231cc88a' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:center right calc(.375em + .1875rem);background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#1cc88a;box-shadow:0 0 0 .2rem rgba(28,200,138,.25)}.form-control.is-valid~.valid-feedback,.form-control.is-valid~.valid-tooltip,.was-validated .form-control:valid~.valid-feedback,.was-validated .form-control:valid~.valid-tooltip{display:block}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-valid,.was-validated .custom-select:valid{border-color:#1cc88a;padding-right:calc((1em + .75rem) * 3 / 4 + 1.75rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%235a5c69' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px,url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%231cc88a' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e") #fff no-repeat center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem)}.custom-select.is-valid:focus,.was-validated .custom-select:valid:focus{border-color:#1cc88a;box-shadow:0 0 0 .2rem rgba(28,200,138,.25)}.custom-select.is-valid~.valid-feedback,.custom-select.is-valid~.valid-tooltip,.was-validated .custom-select:valid~.valid-feedback,.was-validated .custom-select:valid~.valid-tooltip{display:block}.form-control-file.is-valid~.valid-feedback,.form-control-file.is-valid~.valid-tooltip,.was-validated .form-control-file:valid~.valid-feedback,.was-validated .form-control-file:valid~.valid-tooltip{display:block}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#1cc88a}.form-check-input.is-valid~.valid-feedback,.form-check-input.is-valid~.valid-tooltip,.was-validated .form-check-input:valid~.valid-feedback,.was-validated .form-check-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid~.custom-control-label,.was-validated .custom-control-input:valid~.custom-control-label{color:#1cc88a}.custom-control-input.is-valid~.custom-control-label::before,.was-validated .custom-control-input:valid~.custom-control-label::before{border-color:#1cc88a}.custom-control-input.is-valid~.valid-feedback,.custom-control-input.is-valid~.valid-tooltip,.was-validated .custom-control-input:valid~.valid-feedback,.was-validated .custom-control-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid:checked~.custom-control-label::before,.was-validated .custom-control-input:valid:checked~.custom-control-label::before{border-color:#34e3a4;background-color:#34e3a4}.custom-control-input.is-valid:focus~.custom-control-label::before,.was-validated .custom-control-input:valid:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(28,200,138,.25)}.custom-control-input.is-valid:focus:not(:checked)~.custom-control-label::before,.was-validated .custom-control-input:valid:focus:not(:checked)~.custom-control-label::before{border-color:#1cc88a}.custom-file-input.is-valid~.custom-file-label,.was-validated .custom-file-input:valid~.custom-file-label{border-color:#1cc88a}.custom-file-input.is-valid~.valid-feedback,.custom-file-input.is-valid~.valid-tooltip,.was-validated .custom-file-input:valid~.valid-feedback,.was-validated .custom-file-input:valid~.valid-tooltip{display:block}.custom-file-input.is-valid:focus~.custom-file-label,.was-validated .custom-file-input:valid:focus~.custom-file-label{border-color:#1cc88a;box-shadow:0 0 0 .2rem rgba(28,200,138,.25)}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#e74a3b}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(231,74,59,.9);border-radius:.35rem}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#e74a3b;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23e74a3b' viewBox='-2 -2 7 7'%3e%3cpath stroke='%23e74a3b' d='M0 0l3 3m0-3L0 3'/%3e%3ccircle r='.5'/%3e%3ccircle cx='3' r='.5'/%3e%3ccircle cy='3' r='.5'/%3e%3ccircle cx='3' cy='3' r='.5'/%3e%3c/svg%3E");background-repeat:no-repeat;background-position:center right calc(.375em + .1875rem);background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#e74a3b;box-shadow:0 0 0 .2rem rgba(231,74,59,.25)}.form-control.is-invalid~.invalid-feedback,.form-control.is-invalid~.invalid-tooltip,.was-validated .form-control:invalid~.invalid-feedback,.was-validated .form-control:invalid~.invalid-tooltip{display:block}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-invalid,.was-validated .custom-select:invalid{border-color:#e74a3b;padding-right:calc((1em + .75rem) * 3 / 4 + 1.75rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%235a5c69' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px,url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23e74a3b' viewBox='-2 -2 7 7'%3e%3cpath stroke='%23e74a3b' d='M0 0l3 3m0-3L0 3'/%3e%3ccircle r='.5'/%3e%3ccircle cx='3' r='.5'/%3e%3ccircle cy='3' r='.5'/%3e%3ccircle cx='3' cy='3' r='.5'/%3e%3c/svg%3E") #fff no-repeat center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem)}.custom-select.is-invalid:focus,.was-validated .custom-select:invalid:focus{border-color:#e74a3b;box-shadow:0 0 0 .2rem rgba(231,74,59,.25)}.custom-select.is-invalid~.invalid-feedback,.custom-select.is-invalid~.invalid-tooltip,.was-validated .custom-select:invalid~.invalid-feedback,.was-validated .custom-select:invalid~.invalid-tooltip{display:block}.form-control-file.is-invalid~.invalid-feedback,.form-control-file.is-invalid~.invalid-tooltip,.was-validated .form-control-file:invalid~.invalid-feedback,.was-validated .form-control-file:invalid~.invalid-tooltip{display:block}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#e74a3b}.form-check-input.is-invalid~.invalid-feedback,.form-check-input.is-invalid~.invalid-tooltip,.was-validated .form-check-input:invalid~.invalid-feedback,.was-validated .form-check-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid~.custom-control-label,.was-validated .custom-control-input:invalid~.custom-control-label{color:#e74a3b}.custom-control-input.is-invalid~.custom-control-label::before,.was-validated .custom-control-input:invalid~.custom-control-label::before{border-color:#e74a3b}.custom-control-input.is-invalid~.invalid-feedback,.custom-control-input.is-invalid~.invalid-tooltip,.was-validated .custom-control-input:invalid~.invalid-feedback,.was-validated .custom-control-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid:checked~.custom-control-label::before,.was-validated .custom-control-input:invalid:checked~.custom-control-label::before{border-color:#ed7468;background-color:#ed7468}.custom-control-input.is-invalid:focus~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(231,74,59,.25)}.custom-control-input.is-invalid:focus:not(:checked)~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus:not(:checked)~.custom-control-label::before{border-color:#e74a3b}.custom-file-input.is-invalid~.custom-file-label,.was-validated .custom-file-input:invalid~.custom-file-label{border-color:#e74a3b}.custom-file-input.is-invalid~.invalid-feedback,.custom-file-input.is-invalid~.invalid-tooltip,.was-validated .custom-file-input:invalid~.invalid-feedback,.was-validated .custom-file-input:invalid~.invalid-tooltip{display:block}.custom-file-input.is-invalid:focus~.custom-file-label,.was-validated .custom-file-input:invalid:focus~.custom-file-label{border-color:#e74a3b;box-shadow:0 0 0 .2rem rgba(231,74,59,.25)}.form-inline{display:flex;flex-flow:row wrap;align-items:center}.form-inline .form-check{width:100%}@media (min-width:576px){.form-inline label{display:flex;align-items:center;justify-content:center;margin-bottom:0}.form-inline .form-group{display:flex;flex:0 0 auto;flex-flow:row wrap;align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-plaintext{display:inline-block}.form-inline .custom-select,.form-inline .input-group{width:auto}.form-inline .form-check{display:flex;align-items:center;justify-content:center;width:auto;padding-left:0}.form-inline .form-check-input{position:relative;flex-shrink:0;margin-top:0;margin-right:.25rem;margin-left:0}.form-inline .custom-control{align-items:center;justify-content:center}.form-inline .custom-control-label{margin-bottom:0}}.btn{display:inline-block;font-weight:400;color:#858796;text-align:center;vertical-align:middle;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;line-height:1.5;border-radius:.35rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#858796;text-decoration:none}.btn.focus,.btn:focus{outline:0;box-shadow:0 0 0 .2rem rgba(78,115,223,.25)}.btn.disabled,.btn:disabled{opacity:.65}a.btn.disabled,fieldset:disabled a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#4e73df;border-color:#4e73df}.btn-primary:hover{color:#fff;background-color:#2e59d9;border-color:#2653d4}.btn-primary.focus,.btn-primary:focus{box-shadow:0 0 0 .2rem rgba(105,136,228,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#4e73df;border-color:#4e73df}.btn-primary:not(:disabled):not(.disabled).active,.btn-primary:not(:disabled):not(.disabled):active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#2653d4;border-color:#244ec9}.btn-primary:not(:disabled):not(.disabled).active:focus,.btn-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(105,136,228,.5)}.btn-secondary{color:#fff;background-color:#858796;border-color:#858796}.btn-secondary:hover{color:#fff;background-color:#717384;border-color:#6b6d7d}.btn-secondary.focus,.btn-secondary:focus{box-shadow:0 0 0 .2rem rgba(151,153,166,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#858796;border-color:#858796}.btn-secondary:not(:disabled):not(.disabled).active,.btn-secondary:not(:disabled):not(.disabled):active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#6b6d7d;border-color:#656776}.btn-secondary:not(:disabled):not(.disabled).active:focus,.btn-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(151,153,166,.5)}.btn-success{color:#fff;background-color:#1cc88a;border-color:#1cc88a}.btn-success:hover{color:#fff;background-color:#17a673;border-color:#169b6b}.btn-success.focus,.btn-success:focus{box-shadow:0 0 0 .2rem rgba(62,208,156,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#1cc88a;border-color:#1cc88a}.btn-success:not(:disabled):not(.disabled).active,.btn-success:not(:disabled):not(.disabled):active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#169b6b;border-color:#149063}.btn-success:not(:disabled):not(.disabled).active:focus,.btn-success:not(:disabled):not(.disabled):active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(62,208,156,.5)}.btn-info{color:#fff;background-color:#36b9cc;border-color:#36b9cc}.btn-info:hover{color:#fff;background-color:#2c9faf;border-color:#2a96a5}.btn-info.focus,.btn-info:focus{box-shadow:0 0 0 .2rem rgba(84,196,212,.5)}.btn-info.disabled,.btn-info:disabled{color:#fff;background-color:#36b9cc;border-color:#36b9cc}.btn-info:not(:disabled):not(.disabled).active,.btn-info:not(:disabled):not(.disabled):active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#2a96a5;border-color:#278c9b}.btn-info:not(:disabled):not(.disabled).active:focus,.btn-info:not(:disabled):not(.disabled):active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(84,196,212,.5)}.btn-warning{color:#fff;background-color:#f6c23e;border-color:#f6c23e}.btn-warning:hover{color:#fff;background-color:#f4b619;border-color:#f4b30d}.btn-warning.focus,.btn-warning:focus{box-shadow:0 0 0 .2rem rgba(247,203,91,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#fff;background-color:#f6c23e;border-color:#f6c23e}.btn-warning:not(:disabled):not(.disabled).active,.btn-warning:not(:disabled):not(.disabled):active,.show>.btn-warning.dropdown-toggle{color:#fff;background-color:#f4b30d;border-color:#e9aa0b}.btn-warning:not(:disabled):not(.disabled).active:focus,.btn-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(247,203,91,.5)}.btn-danger{color:#fff;background-color:#e74a3b;border-color:#e74a3b}.btn-danger:hover{color:#fff;background-color:#e02d1b;border-color:#d52a1a}.btn-danger.focus,.btn-danger:focus{box-shadow:0 0 0 .2rem rgba(235,101,88,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#e74a3b;border-color:#e74a3b}.btn-danger:not(:disabled):not(.disabled).active,.btn-danger:not(:disabled):not(.disabled):active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#d52a1a;border-color:#ca2819}.btn-danger:not(:disabled):not(.disabled).active:focus,.btn-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(235,101,88,.5)}.btn-light{color:#3a3b45;background-color:#f8f9fc;border-color:#f8f9fc}.btn-light:hover{color:#3a3b45;background-color:#dde2f1;border-color:#d4daed}.btn-light.focus,.btn-light:focus{box-shadow:0 0 0 .2rem rgba(220,221,225,.5)}.btn-light.disabled,.btn-light:disabled{color:#3a3b45;background-color:#f8f9fc;border-color:#f8f9fc}.btn-light:not(:disabled):not(.disabled).active,.btn-light:not(:disabled):not(.disabled):active,.show>.btn-light.dropdown-toggle{color:#3a3b45;background-color:#d4daed;border-color:#cbd3e9}.btn-light:not(:disabled):not(.disabled).active:focus,.btn-light:not(:disabled):not(.disabled):active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(220,221,225,.5)}.btn-dark{color:#fff;background-color:#5a5c69;border-color:#5a5c69}.btn-dark:hover{color:#fff;background-color:#484a54;border-color:#42444e}.btn-dark.focus,.btn-dark:focus{box-shadow:0 0 0 .2rem rgba(115,116,128,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#5a5c69;border-color:#5a5c69}.btn-dark:not(:disabled):not(.disabled).active,.btn-dark:not(:disabled):not(.disabled):active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#42444e;border-color:#3d3e47}.btn-dark:not(:disabled):not(.disabled).active:focus,.btn-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(115,116,128,.5)}.btn-outline-primary{color:#4e73df;border-color:#4e73df}.btn-outline-primary:hover{color:#fff;background-color:#4e73df;border-color:#4e73df}.btn-outline-primary.focus,.btn-outline-primary:focus{box-shadow:0 0 0 .2rem rgba(78,115,223,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#4e73df;background-color:transparent}.btn-outline-primary:not(:disabled):not(.disabled).active,.btn-outline-primary:not(:disabled):not(.disabled):active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#4e73df;border-color:#4e73df}.btn-outline-primary:not(:disabled):not(.disabled).active:focus,.btn-outline-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(78,115,223,.5)}.btn-outline-secondary{color:#858796;border-color:#858796}.btn-outline-secondary:hover{color:#fff;background-color:#858796;border-color:#858796}.btn-outline-secondary.focus,.btn-outline-secondary:focus{box-shadow:0 0 0 .2rem rgba(133,135,150,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#858796;background-color:transparent}.btn-outline-secondary:not(:disabled):not(.disabled).active,.btn-outline-secondary:not(:disabled):not(.disabled):active,.show>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#858796;border-color:#858796}.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(133,135,150,.5)}.btn-outline-success{color:#1cc88a;border-color:#1cc88a}.btn-outline-success:hover{color:#fff;background-color:#1cc88a;border-color:#1cc88a}.btn-outline-success.focus,.btn-outline-success:focus{box-shadow:0 0 0 .2rem rgba(28,200,138,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#1cc88a;background-color:transparent}.btn-outline-success:not(:disabled):not(.disabled).active,.btn-outline-success:not(:disabled):not(.disabled):active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#1cc88a;border-color:#1cc88a}.btn-outline-success:not(:disabled):not(.disabled).active:focus,.btn-outline-success:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(28,200,138,.5)}.btn-outline-info{color:#36b9cc;border-color:#36b9cc}.btn-outline-info:hover{color:#fff;background-color:#36b9cc;border-color:#36b9cc}.btn-outline-info.focus,.btn-outline-info:focus{box-shadow:0 0 0 .2rem rgba(54,185,204,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#36b9cc;background-color:transparent}.btn-outline-info:not(:disabled):not(.disabled).active,.btn-outline-info:not(:disabled):not(.disabled):active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#36b9cc;border-color:#36b9cc}.btn-outline-info:not(:disabled):not(.disabled).active:focus,.btn-outline-info:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(54,185,204,.5)}.btn-outline-warning{color:#f6c23e;border-color:#f6c23e}.btn-outline-warning:hover{color:#fff;background-color:#f6c23e;border-color:#f6c23e}.btn-outline-warning.focus,.btn-outline-warning:focus{box-shadow:0 0 0 .2rem rgba(246,194,62,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#f6c23e;background-color:transparent}.btn-outline-warning:not(:disabled):not(.disabled).active,.btn-outline-warning:not(:disabled):not(.disabled):active,.show>.btn-outline-warning.dropdown-toggle{color:#fff;background-color:#f6c23e;border-color:#f6c23e}.btn-outline-warning:not(:disabled):not(.disabled).active:focus,.btn-outline-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(246,194,62,.5)}.btn-outline-danger{color:#e74a3b;border-color:#e74a3b}.btn-outline-danger:hover{color:#fff;background-color:#e74a3b;border-color:#e74a3b}.btn-outline-danger.focus,.btn-outline-danger:focus{box-shadow:0 0 0 .2rem rgba(231,74,59,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#e74a3b;background-color:transparent}.btn-outline-danger:not(:disabled):not(.disabled).active,.btn-outline-danger:not(:disabled):not(.disabled):active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#e74a3b;border-color:#e74a3b}.btn-outline-danger:not(:disabled):not(.disabled).active:focus,.btn-outline-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(231,74,59,.5)}.btn-outline-light{color:#f8f9fc;border-color:#f8f9fc}.btn-outline-light:hover{color:#3a3b45;background-color:#f8f9fc;border-color:#f8f9fc}.btn-outline-light.focus,.btn-outline-light:focus{box-shadow:0 0 0 .2rem rgba(248,249,252,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fc;background-color:transparent}.btn-outline-light:not(:disabled):not(.disabled).active,.btn-outline-light:not(:disabled):not(.disabled):active,.show>.btn-outline-light.dropdown-toggle{color:#3a3b45;background-color:#f8f9fc;border-color:#f8f9fc}.btn-outline-light:not(:disabled):not(.disabled).active:focus,.btn-outline-light:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(248,249,252,.5)}.btn-outline-dark{color:#5a5c69;border-color:#5a5c69}.btn-outline-dark:hover{color:#fff;background-color:#5a5c69;border-color:#5a5c69}.btn-outline-dark.focus,.btn-outline-dark:focus{box-shadow:0 0 0 .2rem rgba(90,92,105,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#5a5c69;background-color:transparent}.btn-outline-dark:not(:disabled):not(.disabled).active,.btn-outline-dark:not(:disabled):not(.disabled):active,.show>.btn-outline-dark.dropdown-toggle{color:#fff;background-color:#5a5c69;border-color:#5a5c69}.btn-outline-dark:not(:disabled):not(.disabled).active:focus,.btn-outline-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(90,92,105,.5)}.btn-link{font-weight:400;color:#4e73df;text-decoration:none}.btn-link:hover{color:#224abe;text-decoration:underline}.btn-link.focus,.btn-link:focus{text-decoration:underline;box-shadow:none}.btn-link.disabled,.btn-link:disabled{color:#858796;pointer-events:none}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:.5rem}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{position:relative;height:0;overflow:hidden;transition:height .15s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.dropdown,.dropleft,.dropright,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:.85rem;color:#858796;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid #e3e6f0;border-radius:.35rem}.dropdown-menu-left{right:auto;left:0}.dropdown-menu-right{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-left{right:auto;left:0}.dropdown-menu-sm-right{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-left{right:auto;left:0}.dropdown-menu-md-right{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-left{right:auto;left:0}.dropdown-menu-lg-right{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-left{right:auto;left:0}.dropdown-menu-xl-right{right:0;left:auto}}.dropup .dropdown-menu{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-menu{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropright .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropright .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-toggle::after{vertical-align:0}.dropleft .dropdown-menu{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropleft .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropleft .dropdown-toggle::after{display:none}.dropleft .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropleft .dropdown-toggle:empty::after{margin-left:0}.dropleft .dropdown-toggle::before{vertical-align:0}.dropdown-menu[x-placement^=bottom],.dropdown-menu[x-placement^=left],.dropdown-menu[x-placement^=right],.dropdown-menu[x-placement^=top]{right:auto;bottom:auto}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid #eaecf4}.dropdown-item{display:block;width:100%;padding:.25rem 1.5rem;clear:both;font-weight:400;color:#3a3b45;text-align:inherit;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#2e2f37;text-decoration:none;background-color:#f8f9fc}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#4e73df}.dropdown-item.disabled,.dropdown-item:disabled{color:#858796;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1.5rem;margin-bottom:0;font-size:.875rem;color:#858796;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1.5rem;color:#3a3b45}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:1 1 auto}.btn-group-vertical>.btn:hover,.btn-group>.btn:hover{z-index:1}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropright .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropleft .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn:not(:first-child){border-top-left-radius:0;border-top-right-radius:0}.btn-group-toggle>.btn,.btn-group-toggle>.btn-group>.btn{margin-bottom:0}.btn-group-toggle>.btn input[type=checkbox],.btn-group-toggle>.btn input[type=radio],.btn-group-toggle>.btn-group>.btn input[type=checkbox],.btn-group-toggle>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.custom-file,.input-group>.custom-select,.input-group>.form-control,.input-group>.form-control-plaintext{position:relative;flex:1 1 auto;width:1%;margin-bottom:0}.input-group>.custom-file+.custom-file,.input-group>.custom-file+.custom-select,.input-group>.custom-file+.form-control,.input-group>.custom-select+.custom-file,.input-group>.custom-select+.custom-select,.input-group>.custom-select+.form-control,.input-group>.form-control+.custom-file,.input-group>.form-control+.custom-select,.input-group>.form-control+.form-control,.input-group>.form-control-plaintext+.custom-file,.input-group>.form-control-plaintext+.custom-select,.input-group>.form-control-plaintext+.form-control{margin-left:-1px}.input-group>.custom-file .custom-file-input:focus~.custom-file-label,.input-group>.custom-select:focus,.input-group>.form-control:focus{z-index:3}.input-group>.custom-file .custom-file-input:focus{z-index:4}.input-group>.custom-select:not(:last-child),.input-group>.form-control:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-select:not(:first-child),.input-group>.form-control:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.custom-file{display:flex;align-items:center}.input-group>.custom-file:not(:last-child) .custom-file-label,.input-group>.custom-file:not(:last-child) .custom-file-label::after{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-file:not(:first-child) .custom-file-label{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-append,.input-group-prepend{display:flex}.input-group-append .btn,.input-group-prepend .btn{position:relative;z-index:2}.input-group-append .btn:focus,.input-group-prepend .btn:focus{z-index:3}.input-group-append .btn+.btn,.input-group-append .btn+.input-group-text,.input-group-append .input-group-text+.btn,.input-group-append .input-group-text+.input-group-text,.input-group-prepend .btn+.btn,.input-group-prepend .btn+.input-group-text,.input-group-prepend .input-group-text+.btn,.input-group-prepend .input-group-text+.input-group-text{margin-left:-1px}.input-group-prepend{margin-right:-1px}.input-group-append{margin-left:-1px}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;margin-bottom:0;font-size:1rem;font-weight:400;line-height:1.5;color:#6e707e;text-align:center;white-space:nowrap;background-color:#eaecf4;border:1px solid #d1d3e2;border-radius:.35rem}.input-group-text input[type=checkbox],.input-group-text input[type=radio]{margin-top:0}.input-group-lg>.custom-select,.input-group-lg>.form-control:not(textarea){height:calc(1.5em + 1rem + 2px)}.input-group-lg>.custom-select,.input-group-lg>.form-control,.input-group-lg>.input-group-append>.btn,.input-group-lg>.input-group-append>.input-group-text,.input-group-lg>.input-group-prepend>.btn,.input-group-lg>.input-group-prepend>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.input-group-sm>.custom-select,.input-group-sm>.form-control:not(textarea){height:calc(1.5em + .5rem + 2px)}.input-group-sm>.custom-select,.input-group-sm>.form-control,.input-group-sm>.input-group-append>.btn,.input-group-sm>.input-group-append>.input-group-text,.input-group-sm>.input-group-prepend>.btn,.input-group-sm>.input-group-prepend>.input-group-text{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.input-group-lg>.custom-select,.input-group-sm>.custom-select{padding-right:1.75rem}.input-group>.input-group-append:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group>.input-group-append:last-child>.input-group-text:not(:last-child),.input-group>.input-group-append:not(:last-child)>.btn,.input-group>.input-group-append:not(:last-child)>.input-group-text,.input-group>.input-group-prepend>.btn,.input-group>.input-group-prepend>.input-group-text{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.input-group-append>.btn,.input-group>.input-group-append>.input-group-text,.input-group>.input-group-prepend:first-child>.btn:not(:first-child),.input-group>.input-group-prepend:first-child>.input-group-text:not(:first-child),.input-group>.input-group-prepend:not(:first-child)>.btn,.input-group>.input-group-prepend:not(:first-child)>.input-group-text{border-top-left-radius:0;border-bottom-left-radius:0}.custom-control{position:relative;display:block;min-height:1.5rem;padding-left:1.5rem}.custom-control-inline{display:inline-flex;margin-right:1rem}.custom-control-input{position:absolute;z-index:-1;opacity:0}.custom-control-input:checked~.custom-control-label::before{color:#fff;border-color:#4e73df;background-color:#4e73df}.custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(78,115,223,.25)}.custom-control-input:focus:not(:checked)~.custom-control-label::before{border-color:#bac8f3}.custom-control-input:not(:disabled):active~.custom-control-label::before{color:#fff;background-color:#e5ebfa;border-color:#e5ebfa}.custom-control-input:disabled~.custom-control-label{color:#858796}.custom-control-input:disabled~.custom-control-label::before{background-color:#eaecf4}.custom-control-label{position:relative;margin-bottom:0;vertical-align:top}.custom-control-label::before{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;pointer-events:none;content:"";background-color:#fff;border:#b7b9cc solid 1px}.custom-control-label::after{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;content:"";background:no-repeat 50%/50% 50%}.custom-checkbox .custom-control-label::before{border-radius:.35rem}.custom-checkbox .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::before{border-color:#4e73df;background-color:#4e73df}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 4'%3e%3cpath stroke='%23fff' d='M0 2h4'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(78,115,223,.5)}.custom-checkbox .custom-control-input:disabled:indeterminate~.custom-control-label::before{background-color:rgba(78,115,223,.5)}.custom-radio .custom-control-label::before{border-radius:50%}.custom-radio .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.custom-radio .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(78,115,223,.5)}.custom-switch{padding-left:2.25rem}.custom-switch .custom-control-label::before{left:-2.25rem;width:1.75rem;pointer-events:all;border-radius:.5rem}.custom-switch .custom-control-label::after{top:calc(.25rem + 2px);left:calc(-2.25rem + 2px);width:calc(1rem - 4px);height:calc(1rem - 4px);background-color:#b7b9cc;border-radius:.5rem;transition:transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-switch .custom-control-label::after{transition:none}}.custom-switch .custom-control-input:checked~.custom-control-label::after{background-color:#fff;transform:translateX(.75rem)}.custom-switch .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(78,115,223,.5)}.custom-select{display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem 1.75rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#6e707e;vertical-align:middle;background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%235a5c69' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px;background-color:#fff;border:1px solid #d1d3e2;border-radius:.35rem;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-select:focus{border-color:#bac8f3;outline:0;box-shadow:0 0 0 .2rem rgba(78,115,223,.25)}.custom-select:focus::-ms-value{color:#6e707e;background-color:#fff}.custom-select[multiple],.custom-select[size]:not([size="1"]){height:auto;padding-right:.75rem;background-image:none}.custom-select:disabled{color:#858796;background-color:#eaecf4}.custom-select::-ms-expand{display:none}.custom-select-sm{height:calc(1.5em + .5rem + 2px);padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem}.custom-select-lg{height:calc(1.5em + 1rem + 2px);padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem}.custom-file{position:relative;display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);margin-bottom:0}.custom-file-input{position:relative;z-index:2;width:100%;height:calc(1.5em + .75rem + 2px);margin:0;opacity:0}.custom-file-input:focus~.custom-file-label{border-color:#bac8f3;box-shadow:0 0 0 .2rem rgba(78,115,223,.25)}.custom-file-input:disabled~.custom-file-label{background-color:#eaecf4}.custom-file-input:lang(en)~.custom-file-label::after{content:"Browse"}.custom-file-input~.custom-file-label[data-browse]::after{content:attr(data-browse)}.custom-file-label{position:absolute;top:0;right:0;left:0;z-index:1;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;font-weight:400;line-height:1.5;color:#6e707e;background-color:#fff;border:1px solid #d1d3e2;border-radius:.35rem}.custom-file-label::after{position:absolute;top:0;right:0;bottom:0;z-index:3;display:block;height:calc(1.5em + .75rem);padding:.375rem .75rem;line-height:1.5;color:#6e707e;content:"Browse";background-color:#eaecf4;border-left:inherit;border-radius:0 .35rem .35rem 0}.custom-range{width:100%;height:calc(1rem + .4rem);padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-range:focus{outline:0}.custom-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(78,115,223,.25)}.custom-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(78,115,223,.25)}.custom-range:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(78,115,223,.25)}.custom-range::-moz-focus-outer{border:0}.custom-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#4e73df;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-webkit-slider-thumb{transition:none}}.custom-range::-webkit-slider-thumb:active{background-color:#e5ebfa}.custom-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dddfeb;border-color:transparent;border-radius:1rem}.custom-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#4e73df;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-moz-range-thumb{transition:none}}.custom-range::-moz-range-thumb:active{background-color:#e5ebfa}.custom-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dddfeb;border-color:transparent;border-radius:1rem}.custom-range::-ms-thumb{width:1rem;height:1rem;margin-top:0;margin-right:.2rem;margin-left:.2rem;background-color:#4e73df;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-ms-thumb{transition:none}}.custom-range::-ms-thumb:active{background-color:#e5ebfa}.custom-range::-ms-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:transparent;border-color:transparent;border-width:.5rem}.custom-range::-ms-fill-lower{background-color:#dddfeb;border-radius:1rem}.custom-range::-ms-fill-upper{margin-right:15px;background-color:#dddfeb;border-radius:1rem}.custom-range:disabled::-webkit-slider-thumb{background-color:#b7b9cc}.custom-range:disabled::-webkit-slider-runnable-track{cursor:default}.custom-range:disabled::-moz-range-thumb{background-color:#b7b9cc}.custom-range:disabled::-moz-range-track{cursor:default}.custom-range:disabled::-ms-thumb{background-color:#b7b9cc}.custom-control-label::before,.custom-file-label,.custom-select{transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-control-label::before,.custom-file-label,.custom-select{transition:none}}.nav{display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem}.nav-link:focus,.nav-link:hover{text-decoration:none}.nav-link.disabled{color:#858796;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dddfeb}.nav-tabs .nav-item{margin-bottom:-1px}.nav-tabs .nav-link{border:1px solid transparent;border-top-left-radius:.35rem;border-top-right-radius:.35rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#eaecf4 #eaecf4 #dddfeb}.nav-tabs .nav-link.disabled{color:#858796;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#6e707e;background-color:#fff;border-color:#dddfeb #dddfeb #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{border-radius:.35rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#4e73df}.nav-fill .nav-item{flex:1 1 auto;text-align:center}.nav-justified .nav-item{flex-basis:0;flex-grow:1;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding:.5rem 1rem}.navbar>.container,.navbar>.container-fluid{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between}.navbar-brand{display:inline-block;padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;line-height:inherit;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-nav{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static;float:none}.navbar-text{display:inline-block;padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.35rem}.navbar-toggler:focus,.navbar-toggler:hover{text-decoration:none}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:"";background:no-repeat center center;background-size:100% 100%}@media (max-width:575.98px){.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:576px){.navbar-expand-sm{flex-flow:row nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid{flex-wrap:nowrap}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media (max-width:767.98px){.navbar-expand-md>.container,.navbar-expand-md>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:768px){.navbar-expand-md{flex-flow:row nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md>.container,.navbar-expand-md>.container-fluid{flex-wrap:nowrap}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media (max-width:991.98px){.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:992px){.navbar-expand-lg{flex-flow:row nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid{flex-wrap:nowrap}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media (max-width:1199.98px){.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:1200px){.navbar-expand-xl{flex-flow:row nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid{flex-wrap:nowrap}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}.navbar-expand{flex-flow:row nowrap;justify-content:flex-start}.navbar-expand>.container,.navbar-expand>.container-fluid{padding-right:0;padding-left:0}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand>.container,.navbar-expand>.container-fluid{flex-wrap:nowrap}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.5)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.5);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3e%3cpath stroke='rgba(0, 0, 0, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(0,0,0,.5)}.navbar-light .navbar-text a{color:rgba(0,0,0,.9)}.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.5)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.5);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3e%3cpath stroke='rgba(255, 255, 255, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,.5)}.navbar-dark .navbar-text a{color:#fff}.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:flex;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid #e3e6f0;border-radius:.35rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group:first-child .list-group-item:first-child{border-top-left-radius:.35rem;border-top-right-radius:.35rem}.card>.list-group:last-child .list-group-item:last-child{border-bottom-right-radius:.35rem;border-bottom-left-radius:.35rem}.card-body{flex:1 1 auto;padding:1.25rem}.card-title{margin-bottom:.75rem}.card-subtitle{margin-top:-.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card-header{padding:.75rem 1.25rem;margin-bottom:0;background-color:#f8f9fc;border-bottom:1px solid #e3e6f0}.card-header:first-child{border-radius:calc(.35rem - 1px) calc(.35rem - 1px) 0 0}.card-header+.list-group .list-group-item:first-child{border-top:0}.card-footer{padding:.75rem 1.25rem;background-color:#f8f9fc;border-top:1px solid #e3e6f0}.card-footer:last-child{border-radius:0 0 calc(.35rem - 1px) calc(.35rem - 1px)}.card-header-tabs{margin-right:-.625rem;margin-bottom:-.75rem;margin-left:-.625rem;border-bottom:0}.card-header-pills{margin-right:-.625rem;margin-left:-.625rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem}.card-img{width:100%;border-radius:calc(.35rem - 1px)}.card-img-top{width:100%;border-top-left-radius:calc(.35rem - 1px);border-top-right-radius:calc(.35rem - 1px)}.card-img-bottom{width:100%;border-bottom-right-radius:calc(.35rem - 1px);border-bottom-left-radius:calc(.35rem - 1px)}.card-deck{display:flex;flex-direction:column}.card-deck .card{margin-bottom:.75rem}@media (min-width:576px){.card-deck{flex-flow:row wrap;margin-right:-.75rem;margin-left:-.75rem}.card-deck .card{display:flex;flex:1 0 0%;flex-direction:column;margin-right:.75rem;margin-bottom:0;margin-left:.75rem}}.card-group{display:flex;flex-direction:column}.card-group>.card{margin-bottom:.75rem}@media (min-width:576px){.card-group{flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.card-columns .card{margin-bottom:.75rem}@media (min-width:576px){.card-columns{-moz-column-count:3;column-count:3;-moz-column-gap:1.25rem;column-gap:1.25rem;orphans:1;widows:1}.card-columns .card{display:inline-block;width:100%}}.accordion>.card{overflow:hidden}.accordion>.card:not(:first-of-type) .card-header:first-child{border-radius:0}.accordion>.card:not(:first-of-type):not(:last-of-type){border-bottom:0;border-radius:0}.accordion>.card:first-of-type{border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.accordion>.card:last-of-type{border-top-left-radius:0;border-top-right-radius:0}.accordion>.card .card-header{margin-bottom:-1px}.breadcrumb{display:flex;flex-wrap:wrap;padding:.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#eaecf4;border-radius:.35rem}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{display:inline-block;padding-right:.5rem;color:#858796;content:"/"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#858796}.pagination{display:flex;padding-left:0;list-style:none;border-radius:.35rem}.page-link{position:relative;display:block;padding:.5rem .75rem;margin-left:-1px;line-height:1.25;color:#4e73df;background-color:#fff;border:1px solid #dddfeb}.page-link:hover{z-index:2;color:#224abe;text-decoration:none;background-color:#eaecf4;border-color:#dddfeb}.page-link:focus{z-index:2;outline:0;box-shadow:0 0 0 .2rem rgba(78,115,223,.25)}.page-item:first-child .page-link{margin-left:0;border-top-left-radius:.35rem;border-bottom-left-radius:.35rem}.page-item:last-child .page-link{border-top-right-radius:.35rem;border-bottom-right-radius:.35rem}.page-item.active .page-link{z-index:1;color:#fff;background-color:#4e73df;border-color:#4e73df}.page-item.disabled .page-link{color:#858796;pointer-events:none;cursor:auto;background-color:#fff;border-color:#dddfeb}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem;line-height:1.5}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem;line-height:1.5}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.25em .4em;font-size:75%;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.35rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.badge{transition:none}}a.badge:focus,a.badge:hover{text-decoration:none}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{padding-right:.6em;padding-left:.6em;border-radius:10rem}.badge-primary{color:#fff;background-color:#4e73df}a.badge-primary:focus,a.badge-primary:hover{color:#fff;background-color:#2653d4}a.badge-primary.focus,a.badge-primary:focus{outline:0;box-shadow:0 0 0 .2rem rgba(78,115,223,.5)}.badge-secondary{color:#fff;background-color:#858796}a.badge-secondary:focus,a.badge-secondary:hover{color:#fff;background-color:#6b6d7d}a.badge-secondary.focus,a.badge-secondary:focus{outline:0;box-shadow:0 0 0 .2rem rgba(133,135,150,.5)}.badge-success{color:#fff;background-color:#1cc88a}a.badge-success:focus,a.badge-success:hover{color:#fff;background-color:#169b6b}a.badge-success.focus,a.badge-success:focus{outline:0;box-shadow:0 0 0 .2rem rgba(28,200,138,.5)}.badge-info{color:#fff;background-color:#36b9cc}a.badge-info:focus,a.badge-info:hover{color:#fff;background-color:#2a96a5}a.badge-info.focus,a.badge-info:focus{outline:0;box-shadow:0 0 0 .2rem rgba(54,185,204,.5)}.badge-warning{color:#fff;background-color:#f6c23e}a.badge-warning:focus,a.badge-warning:hover{color:#fff;background-color:#f4b30d}a.badge-warning.focus,a.badge-warning:focus{outline:0;box-shadow:0 0 0 .2rem rgba(246,194,62,.5)}.badge-danger{color:#fff;background-color:#e74a3b}a.badge-danger:focus,a.badge-danger:hover{color:#fff;background-color:#d52a1a}a.badge-danger.focus,a.badge-danger:focus{outline:0;box-shadow:0 0 0 .2rem rgba(231,74,59,.5)}.badge-light{color:#3a3b45;background-color:#f8f9fc}a.badge-light:focus,a.badge-light:hover{color:#3a3b45;background-color:#d4daed}a.badge-light.focus,a.badge-light:focus{outline:0;box-shadow:0 0 0 .2rem rgba(248,249,252,.5)}.badge-dark{color:#fff;background-color:#5a5c69}a.badge-dark:focus,a.badge-dark:hover{color:#fff;background-color:#42444e}a.badge-dark.focus,a.badge-dark:focus{outline:0;box-shadow:0 0 0 .2rem rgba(90,92,105,.5)}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#eaecf4;border-radius:.3rem}@media (min-width:576px){.jumbotron{padding:4rem 2rem}}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{position:relative;padding:.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.35rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:4rem}.alert-dismissible .close{position:absolute;top:0;right:0;padding:.75rem 1.25rem;color:inherit}.alert-primary{color:#293c74;background-color:#dce3f9;border-color:#cdd8f6}.alert-primary hr{border-top-color:#b7c7f2}.alert-primary .alert-link{color:#1c294e}.alert-secondary{color:#45464e;background-color:#e7e7ea;border-color:#dddde2}.alert-secondary hr{border-top-color:#cfcfd6}.alert-secondary .alert-link{color:#2d2e33}.alert-success{color:#0f6848;background-color:#d2f4e8;border-color:#bff0de}.alert-success hr{border-top-color:#aaebd3}.alert-success .alert-link{color:#093b29}.alert-info{color:#1c606a;background-color:#d7f1f5;border-color:#c7ebf1}.alert-info hr{border-top-color:#b3e4ec}.alert-info .alert-link{color:#113b42}.alert-warning{color:#806520;background-color:#fdf3d8;border-color:#fceec9}.alert-warning hr{border-top-color:#fbe6b1}.alert-warning .alert-link{color:#574516}.alert-danger{color:#78261f;background-color:#fadbd8;border-color:#f8ccc8}.alert-danger hr{border-top-color:#f5b7b1}.alert-danger .alert-link{color:#4f1915}.alert-light{color:#818183;background-color:#fefefe;border-color:#fdfdfe}.alert-light hr{border-top-color:#ececf6}.alert-light .alert-link{color:#686869}.alert-dark{color:#2f3037;background-color:#dedee1;border-color:#d1d1d5}.alert-dark hr{border-top-color:#c4c4c9}.alert-dark .alert-link{color:#18181c}@-webkit-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#eaecf4;border-radius:.35rem}.progress-bar{display:flex;flex-direction:column;justify-content:center;color:#fff;text-align:center;white-space:nowrap;background-color:#4e73df;transition:width .6s ease}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:progress-bar-stripes 1s linear infinite;animation:progress-bar-stripes 1s linear infinite}@media (prefers-reduced-motion:reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.media{display:flex;align-items:flex-start}.media-body{flex:1}.list-group{display:flex;flex-direction:column;padding-left:0;margin-bottom:0}.list-group-item-action{width:100%;color:#6e707e;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:#6e707e;text-decoration:none;background-color:#f8f9fc}.list-group-item-action:active{color:#858796;background-color:#eaecf4}.list-group-item{position:relative;display:block;padding:.75rem 1.25rem;margin-bottom:-1px;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:.35rem;border-top-right-radius:.35rem}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:.35rem;border-bottom-left-radius:.35rem}.list-group-item.disabled,.list-group-item:disabled{color:#858796;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#4e73df;border-color:#4e73df}.list-group-horizontal{flex-direction:row}.list-group-horizontal .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal .list-group-item:first-child{border-top-left-radius:.35rem;border-bottom-left-radius:.35rem;border-top-right-radius:0}.list-group-horizontal .list-group-item:last-child{margin-right:0;border-top-right-radius:.35rem;border-bottom-right-radius:.35rem;border-bottom-left-radius:0}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal-sm .list-group-item:first-child{border-top-left-radius:.35rem;border-bottom-left-radius:.35rem;border-top-right-radius:0}.list-group-horizontal-sm .list-group-item:last-child{margin-right:0;border-top-right-radius:.35rem;border-bottom-right-radius:.35rem;border-bottom-left-radius:0}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal-md .list-group-item:first-child{border-top-left-radius:.35rem;border-bottom-left-radius:.35rem;border-top-right-radius:0}.list-group-horizontal-md .list-group-item:last-child{margin-right:0;border-top-right-radius:.35rem;border-bottom-right-radius:.35rem;border-bottom-left-radius:0}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal-lg .list-group-item:first-child{border-top-left-radius:.35rem;border-bottom-left-radius:.35rem;border-top-right-radius:0}.list-group-horizontal-lg .list-group-item:last-child{margin-right:0;border-top-right-radius:.35rem;border-bottom-right-radius:.35rem;border-bottom-left-radius:0}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal-xl .list-group-item:first-child{border-top-left-radius:.35rem;border-bottom-left-radius:.35rem;border-top-right-radius:0}.list-group-horizontal-xl .list-group-item:last-child{margin-right:0;border-top-right-radius:.35rem;border-bottom-right-radius:.35rem;border-bottom-left-radius:0}}.list-group-flush .list-group-item{border-right:0;border-left:0;border-radius:0}.list-group-flush .list-group-item:last-child{margin-bottom:-1px}.list-group-flush:first-child .list-group-item:first-child{border-top:0}.list-group-flush:last-child .list-group-item:last-child{margin-bottom:0;border-bottom:0}.list-group-item-primary{color:#293c74;background-color:#cdd8f6}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#293c74;background-color:#b7c7f2}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#293c74;border-color:#293c74}.list-group-item-secondary{color:#45464e;background-color:#dddde2}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#45464e;background-color:#cfcfd6}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#45464e;border-color:#45464e}.list-group-item-success{color:#0f6848;background-color:#bff0de}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#0f6848;background-color:#aaebd3}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#0f6848;border-color:#0f6848}.list-group-item-info{color:#1c606a;background-color:#c7ebf1}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#1c606a;background-color:#b3e4ec}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#1c606a;border-color:#1c606a}.list-group-item-warning{color:#806520;background-color:#fceec9}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#806520;background-color:#fbe6b1}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#806520;border-color:#806520}.list-group-item-danger{color:#78261f;background-color:#f8ccc8}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#78261f;background-color:#f5b7b1}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#78261f;border-color:#78261f}.list-group-item-light{color:#818183;background-color:#fdfdfe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#818183;background-color:#ececf6}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#818183;border-color:#818183}.list-group-item-dark{color:#2f3037;background-color:#d1d1d5}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#2f3037;background-color:#c4c4c9}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#2f3037;border-color:#2f3037}.close{float:right;font-size:1.5rem;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.5}.close:hover{color:#000;text-decoration:none}.close:not(:disabled):not(.disabled):focus,.close:not(:disabled):not(.disabled):hover{opacity:.75}button.close{padding:0;background-color:transparent;border:0;-webkit-appearance:none;-moz-appearance:none;appearance:none}a.close.disabled{pointer-events:none}.toast{max-width:350px;overflow:hidden;font-size:.875rem;background-color:rgba(255,255,255,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .25rem .75rem rgba(0,0,0,.1);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);opacity:0;border-radius:.25rem}.toast:not(:last-child){margin-bottom:.75rem}.toast.showing{opacity:1}.toast.show{display:block;opacity:1}.toast.hide{display:none}.toast-header{display:flex;align-items:center;padding:.25rem .75rem;color:#858796;background-color:rgba(255,255,255,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05)}.toast-body{padding:.75rem}.modal-open{overflow:hidden}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal{position:fixed;top:0;left:0;z-index:1050;display:none;width:100%;height:100%;overflow:hidden;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal-dialog-scrollable{display:flex;max-height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 1rem);overflow:hidden}.modal-dialog-scrollable .modal-footer,.modal-dialog-scrollable .modal-header{flex-shrink:0}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - 1rem)}.modal-dialog-centered::before{display:block;height:calc(100vh - 1rem);content:""}.modal-dialog-centered.modal-dialog-scrollable{flex-direction:column;justify-content:center;height:100%}.modal-dialog-centered.modal-dialog-scrollable .modal-content{max-height:none}.modal-dialog-centered.modal-dialog-scrollable::before{content:none}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:flex;align-items:flex-start;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #e3e6f0;border-top-left-radius:.3rem;border-top-right-radius:.3rem}.modal-header .close{padding:1rem 1rem;margin:-1rem -1rem -1rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;flex:1 1 auto;padding:1rem}.modal-footer{display:flex;align-items:center;justify-content:flex-end;padding:1rem;border-top:1px solid #e3e6f0;border-bottom-right-radius:.3rem;border-bottom-left-radius:.3rem}.modal-footer>:not(:first-child){margin-left:.25rem}.modal-footer>:not(:last-child){margin-right:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{max-height:calc(100% - 3.5rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-dialog-centered::before{height:calc(100vh - 3.5rem)}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.tooltip{position:absolute;z-index:1070;display:block;margin:0;font-family:Nunito,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[x-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[x-placement^=top] .arrow,.bs-tooltip-top .arrow{bottom:0}.bs-tooltip-auto[x-placement^=top] .arrow::before,.bs-tooltip-top .arrow::before{top:0;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[x-placement^=right],.bs-tooltip-right{padding:0 .4rem}.bs-tooltip-auto[x-placement^=right] .arrow,.bs-tooltip-right .arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=right] .arrow::before,.bs-tooltip-right .arrow::before{right:0;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[x-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[x-placement^=bottom] .arrow,.bs-tooltip-bottom .arrow{top:0}.bs-tooltip-auto[x-placement^=bottom] .arrow::before,.bs-tooltip-bottom .arrow::before{bottom:0;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[x-placement^=left],.bs-tooltip-left{padding:0 .4rem}.bs-tooltip-auto[x-placement^=left] .arrow,.bs-tooltip-left .arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=left] .arrow::before,.bs-tooltip-left .arrow::before{left:0;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.35rem}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;font-family:Nunito,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .arrow{position:absolute;display:block;width:1rem;height:.5rem;margin:0 .3rem}.popover .arrow::after,.popover .arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[x-placement^=top],.bs-popover-top{margin-bottom:.5rem}.bs-popover-auto[x-placement^=top]>.arrow,.bs-popover-top>.arrow{bottom:calc((.5rem + 1px) * -1)}.bs-popover-auto[x-placement^=top]>.arrow::before,.bs-popover-top>.arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=top]>.arrow::after,.bs-popover-top>.arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-auto[x-placement^=right],.bs-popover-right{margin-left:.5rem}.bs-popover-auto[x-placement^=right]>.arrow,.bs-popover-right>.arrow{left:calc((.5rem + 1px) * -1);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=right]>.arrow::before,.bs-popover-right>.arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=right]>.arrow::after,.bs-popover-right>.arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-auto[x-placement^=bottom],.bs-popover-bottom{margin-top:.5rem}.bs-popover-auto[x-placement^=bottom]>.arrow,.bs-popover-bottom>.arrow{top:calc((.5rem + 1px) * -1)}.bs-popover-auto[x-placement^=bottom]>.arrow::before,.bs-popover-bottom>.arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=bottom]>.arrow::after,.bs-popover-bottom>.arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-auto[x-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f7f7f7}.bs-popover-auto[x-placement^=left],.bs-popover-left{margin-right:.5rem}.bs-popover-auto[x-placement^=left]>.arrow,.bs-popover-left>.arrow{right:calc((.5rem + 1px) * -1);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=left]>.arrow::before,.bs-popover-left>.arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=left]>.arrow::after,.bs-popover-left>.arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem .75rem;margin-bottom:0;font-size:1rem;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:.5rem .75rem;color:#858796}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-right,.carousel-item-next:not(.carousel-item-left){transform:translateX(100%)}.active.carousel-item-left,.carousel-item-prev:not(.carousel-item-right){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-left,.carousel-fade .carousel-item-prev.carousel-item-right,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{z-index:0;opacity:0;transition:0s .6s opacity}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;color:#fff;text-align:center;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:20px;height:20px;background:no-repeat 50%/100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3e%3cpath d='M5.25 0l-4 4 4 4 1.5-1.5-2.5-2.5 2.5-2.5-1.5-1.5z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3e%3cpath d='M2.75 0l-1.5 1.5 2.5 2.5-2.5 2.5 1.5 1.5 4-4-4-4z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:15;display:flex;justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators li{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center}@-webkit-keyframes spinner-border{to{transform:rotate(360deg)}}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:spinner-border .75s linear infinite;animation:spinner-border .75s linear infinite}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@-webkit-keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1}}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:spinner-grow .75s linear infinite;animation:spinner-grow .75s linear infinite}.spinner-grow-sm{width:1rem;height:1rem}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.bg-primary{background-color:#4e73df!important}a.bg-primary:focus,a.bg-primary:hover,button.bg-primary:focus,button.bg-primary:hover{background-color:#2653d4!important}.bg-secondary{background-color:#858796!important}a.bg-secondary:focus,a.bg-secondary:hover,button.bg-secondary:focus,button.bg-secondary:hover{background-color:#6b6d7d!important}.bg-success{background-color:#1cc88a!important}a.bg-success:focus,a.bg-success:hover,button.bg-success:focus,button.bg-success:hover{background-color:#169b6b!important}.bg-info{background-color:#36b9cc!important}a.bg-info:focus,a.bg-info:hover,button.bg-info:focus,button.bg-info:hover{background-color:#2a96a5!important}.bg-warning{background-color:#f6c23e!important}a.bg-warning:focus,a.bg-warning:hover,button.bg-warning:focus,button.bg-warning:hover{background-color:#f4b30d!important}.bg-danger{background-color:#e74a3b!important}a.bg-danger:focus,a.bg-danger:hover,button.bg-danger:focus,button.bg-danger:hover{background-color:#d52a1a!important}.bg-light{background-color:#f8f9fc!important}a.bg-light:focus,a.bg-light:hover,button.bg-light:focus,button.bg-light:hover{background-color:#d4daed!important}.bg-dark{background-color:#5a5c69!important}a.bg-dark:focus,a.bg-dark:hover,button.bg-dark:focus,button.bg-dark:hover{background-color:#42444e!important}.bg-white{background-color:#fff!important}.bg-transparent{background-color:transparent!important}.border{border:1px solid #e3e6f0!important}.border-top{border-top:1px solid #e3e6f0!important}.border-right{border-right:1px solid #e3e6f0!important}.border-bottom{border-bottom:1px solid #e3e6f0!important}.border-left{border-left:1px solid #e3e6f0!important}.border-0{border:0!important}.border-top-0{border-top:0!important}.border-right-0{border-right:0!important}.border-bottom-0{border-bottom:0!important}.border-left-0{border-left:0!important}.border-primary{border-color:#4e73df!important}.border-secondary{border-color:#858796!important}.border-success{border-color:#1cc88a!important}.border-info{border-color:#36b9cc!important}.border-warning{border-color:#f6c23e!important}.border-danger{border-color:#e74a3b!important}.border-light{border-color:#f8f9fc!important}.border-dark{border-color:#5a5c69!important}.border-white{border-color:#fff!important}.rounded-sm{border-radius:.2rem!important}.rounded{border-radius:.35rem!important}.rounded-top{border-top-left-radius:.35rem!important;border-top-right-radius:.35rem!important}.rounded-right{border-top-right-radius:.35rem!important;border-bottom-right-radius:.35rem!important}.rounded-bottom{border-bottom-right-radius:.35rem!important;border-bottom-left-radius:.35rem!important}.rounded-left{border-top-left-radius:.35rem!important;border-bottom-left-radius:.35rem!important}.rounded-lg{border-radius:.3rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-0{border-radius:0!important}.clearfix::after{display:block;clear:both;content:""}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}@media (min-width:576px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}}@media (min-width:768px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}}@media (min-width:1200px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}}@media print{.d-print-none{display:none!important}.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:""}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9::before{padding-top:42.85714%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-4by3::before{padding-top:75%}.embed-responsive-1by1::before{padding-top:100%}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.flex-fill{flex:1 1 auto!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}@media (min-width:576px){.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}}@media (min-width:768px){.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}}@media (min-width:992px){.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}}@media (min-width:1200px){.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}}.float-left{float:left!important}.float-right{float:right!important}.float-none{float:none!important}@media (min-width:576px){.float-sm-left{float:left!important}.float-sm-right{float:right!important}.float-sm-none{float:none!important}}@media (min-width:768px){.float-md-left{float:left!important}.float-md-right{float:right!important}.float-md-none{float:none!important}}@media (min-width:992px){.float-lg-left{float:left!important}.float-lg-right{float:right!important}.float-lg-none{float:none!important}}@media (min-width:1200px){.float-xl-left{float:left!important}.float-xl-right{float:right!important}.float-xl-none{float:none!important}}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}@supports ((position:-webkit-sticky) or (position:sticky)){.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.sr-only{position:absolute;width:1px;height:1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;overflow:visible;clip:auto;white-space:normal}.shadow-sm{box-shadow:0 .125rem .25rem 0 rgba(58,59,69,.2)!important}.shadow{box-shadow:0 .15rem 1.75rem 0 rgba(58,59,69,.15)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mw-100{max-width:100%!important}.mh-100{max-height:100%!important}.min-vw-100{min-width:100vw!important}.min-vh-100{min-height:100vh!important}.vw-100{width:100vw!important}.vh-100{height:100vh!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;pointer-events:auto;content:"";background-color:rgba(0,0,0,0)}.m-0{margin:0!important}.mt-0,.my-0{margin-top:0!important}.mr-0,.mx-0{margin-right:0!important}.mb-0,.my-0{margin-bottom:0!important}.ml-0,.mx-0{margin-left:0!important}.m-1{margin:.25rem!important}.mt-1,.my-1{margin-top:.25rem!important}.mr-1,.mx-1{margin-right:.25rem!important}.mb-1,.my-1{margin-bottom:.25rem!important}.ml-1,.mx-1{margin-left:.25rem!important}.m-2{margin:.5rem!important}.mt-2,.my-2{margin-top:.5rem!important}.mr-2,.mx-2{margin-right:.5rem!important}.mb-2,.my-2{margin-bottom:.5rem!important}.ml-2,.mx-2{margin-left:.5rem!important}.m-3{margin:1rem!important}.mt-3,.my-3{margin-top:1rem!important}.mr-3,.mx-3{margin-right:1rem!important}.mb-3,.my-3{margin-bottom:1rem!important}.ml-3,.mx-3{margin-left:1rem!important}.m-4{margin:1.5rem!important}.mt-4,.my-4{margin-top:1.5rem!important}.mr-4,.mx-4{margin-right:1.5rem!important}.mb-4,.my-4{margin-bottom:1.5rem!important}.ml-4,.mx-4{margin-left:1.5rem!important}.m-5{margin:3rem!important}.mt-5,.my-5{margin-top:3rem!important}.mr-5,.mx-5{margin-right:3rem!important}.mb-5,.my-5{margin-bottom:3rem!important}.ml-5,.mx-5{margin-left:3rem!important}.p-0{padding:0!important}.pt-0,.py-0{padding-top:0!important}.pr-0,.px-0{padding-right:0!important}.pb-0,.py-0{padding-bottom:0!important}.pl-0,.px-0{padding-left:0!important}.p-1{padding:.25rem!important}.pt-1,.py-1{padding-top:.25rem!important}.pr-1,.px-1{padding-right:.25rem!important}.pb-1,.py-1{padding-bottom:.25rem!important}.pl-1,.px-1{padding-left:.25rem!important}.p-2{padding:.5rem!important}.pt-2,.py-2{padding-top:.5rem!important}.pr-2,.px-2{padding-right:.5rem!important}.pb-2,.py-2{padding-bottom:.5rem!important}.pl-2,.px-2{padding-left:.5rem!important}.p-3{padding:1rem!important}.pt-3,.py-3{padding-top:1rem!important}.pr-3,.px-3{padding-right:1rem!important}.pb-3,.py-3{padding-bottom:1rem!important}.pl-3,.px-3{padding-left:1rem!important}.p-4{padding:1.5rem!important}.pt-4,.py-4{padding-top:1.5rem!important}.pr-4,.px-4{padding-right:1.5rem!important}.pb-4,.py-4{padding-bottom:1.5rem!important}.pl-4,.px-4{padding-left:1.5rem!important}.p-5{padding:3rem!important}.pt-5,.py-5{padding-top:3rem!important}.pr-5,.px-5{padding-right:3rem!important}.pb-5,.py-5{padding-bottom:3rem!important}.pl-5,.px-5{padding-left:3rem!important}.m-n1{margin:-.25rem!important}.mt-n1,.my-n1{margin-top:-.25rem!important}.mr-n1,.mx-n1{margin-right:-.25rem!important}.mb-n1,.my-n1{margin-bottom:-.25rem!important}.ml-n1,.mx-n1{margin-left:-.25rem!important}.m-n2{margin:-.5rem!important}.mt-n2,.my-n2{margin-top:-.5rem!important}.mr-n2,.mx-n2{margin-right:-.5rem!important}.mb-n2,.my-n2{margin-bottom:-.5rem!important}.ml-n2,.mx-n2{margin-left:-.5rem!important}.m-n3{margin:-1rem!important}.mt-n3,.my-n3{margin-top:-1rem!important}.mr-n3,.mx-n3{margin-right:-1rem!important}.mb-n3,.my-n3{margin-bottom:-1rem!important}.ml-n3,.mx-n3{margin-left:-1rem!important}.m-n4{margin:-1.5rem!important}.mt-n4,.my-n4{margin-top:-1.5rem!important}.mr-n4,.mx-n4{margin-right:-1.5rem!important}.mb-n4,.my-n4{margin-bottom:-1.5rem!important}.ml-n4,.mx-n4{margin-left:-1.5rem!important}.m-n5{margin:-3rem!important}.mt-n5,.my-n5{margin-top:-3rem!important}.mr-n5,.mx-n5{margin-right:-3rem!important}.mb-n5,.my-n5{margin-bottom:-3rem!important}.ml-n5,.mx-n5{margin-left:-3rem!important}.m-auto{margin:auto!important}.mt-auto,.my-auto{margin-top:auto!important}.mr-auto,.mx-auto{margin-right:auto!important}.mb-auto,.my-auto{margin-bottom:auto!important}.ml-auto,.mx-auto{margin-left:auto!important}@media (min-width:576px){.m-sm-0{margin:0!important}.mt-sm-0,.my-sm-0{margin-top:0!important}.mr-sm-0,.mx-sm-0{margin-right:0!important}.mb-sm-0,.my-sm-0{margin-bottom:0!important}.ml-sm-0,.mx-sm-0{margin-left:0!important}.m-sm-1{margin:.25rem!important}.mt-sm-1,.my-sm-1{margin-top:.25rem!important}.mr-sm-1,.mx-sm-1{margin-right:.25rem!important}.mb-sm-1,.my-sm-1{margin-bottom:.25rem!important}.ml-sm-1,.mx-sm-1{margin-left:.25rem!important}.m-sm-2{margin:.5rem!important}.mt-sm-2,.my-sm-2{margin-top:.5rem!important}.mr-sm-2,.mx-sm-2{margin-right:.5rem!important}.mb-sm-2,.my-sm-2{margin-bottom:.5rem!important}.ml-sm-2,.mx-sm-2{margin-left:.5rem!important}.m-sm-3{margin:1rem!important}.mt-sm-3,.my-sm-3{margin-top:1rem!important}.mr-sm-3,.mx-sm-3{margin-right:1rem!important}.mb-sm-3,.my-sm-3{margin-bottom:1rem!important}.ml-sm-3,.mx-sm-3{margin-left:1rem!important}.m-sm-4{margin:1.5rem!important}.mt-sm-4,.my-sm-4{margin-top:1.5rem!important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem!important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem!important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem!important}.m-sm-5{margin:3rem!important}.mt-sm-5,.my-sm-5{margin-top:3rem!important}.mr-sm-5,.mx-sm-5{margin-right:3rem!important}.mb-sm-5,.my-sm-5{margin-bottom:3rem!important}.ml-sm-5,.mx-sm-5{margin-left:3rem!important}.p-sm-0{padding:0!important}.pt-sm-0,.py-sm-0{padding-top:0!important}.pr-sm-0,.px-sm-0{padding-right:0!important}.pb-sm-0,.py-sm-0{padding-bottom:0!important}.pl-sm-0,.px-sm-0{padding-left:0!important}.p-sm-1{padding:.25rem!important}.pt-sm-1,.py-sm-1{padding-top:.25rem!important}.pr-sm-1,.px-sm-1{padding-right:.25rem!important}.pb-sm-1,.py-sm-1{padding-bottom:.25rem!important}.pl-sm-1,.px-sm-1{padding-left:.25rem!important}.p-sm-2{padding:.5rem!important}.pt-sm-2,.py-sm-2{padding-top:.5rem!important}.pr-sm-2,.px-sm-2{padding-right:.5rem!important}.pb-sm-2,.py-sm-2{padding-bottom:.5rem!important}.pl-sm-2,.px-sm-2{padding-left:.5rem!important}.p-sm-3{padding:1rem!important}.pt-sm-3,.py-sm-3{padding-top:1rem!important}.pr-sm-3,.px-sm-3{padding-right:1rem!important}.pb-sm-3,.py-sm-3{padding-bottom:1rem!important}.pl-sm-3,.px-sm-3{padding-left:1rem!important}.p-sm-4{padding:1.5rem!important}.pt-sm-4,.py-sm-4{padding-top:1.5rem!important}.pr-sm-4,.px-sm-4{padding-right:1.5rem!important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem!important}.pl-sm-4,.px-sm-4{padding-left:1.5rem!important}.p-sm-5{padding:3rem!important}.pt-sm-5,.py-sm-5{padding-top:3rem!important}.pr-sm-5,.px-sm-5{padding-right:3rem!important}.pb-sm-5,.py-sm-5{padding-bottom:3rem!important}.pl-sm-5,.px-sm-5{padding-left:3rem!important}.m-sm-n1{margin:-.25rem!important}.mt-sm-n1,.my-sm-n1{margin-top:-.25rem!important}.mr-sm-n1,.mx-sm-n1{margin-right:-.25rem!important}.mb-sm-n1,.my-sm-n1{margin-bottom:-.25rem!important}.ml-sm-n1,.mx-sm-n1{margin-left:-.25rem!important}.m-sm-n2{margin:-.5rem!important}.mt-sm-n2,.my-sm-n2{margin-top:-.5rem!important}.mr-sm-n2,.mx-sm-n2{margin-right:-.5rem!important}.mb-sm-n2,.my-sm-n2{margin-bottom:-.5rem!important}.ml-sm-n2,.mx-sm-n2{margin-left:-.5rem!important}.m-sm-n3{margin:-1rem!important}.mt-sm-n3,.my-sm-n3{margin-top:-1rem!important}.mr-sm-n3,.mx-sm-n3{margin-right:-1rem!important}.mb-sm-n3,.my-sm-n3{margin-bottom:-1rem!important}.ml-sm-n3,.mx-sm-n3{margin-left:-1rem!important}.m-sm-n4{margin:-1.5rem!important}.mt-sm-n4,.my-sm-n4{margin-top:-1.5rem!important}.mr-sm-n4,.mx-sm-n4{margin-right:-1.5rem!important}.mb-sm-n4,.my-sm-n4{margin-bottom:-1.5rem!important}.ml-sm-n4,.mx-sm-n4{margin-left:-1.5rem!important}.m-sm-n5{margin:-3rem!important}.mt-sm-n5,.my-sm-n5{margin-top:-3rem!important}.mr-sm-n5,.mx-sm-n5{margin-right:-3rem!important}.mb-sm-n5,.my-sm-n5{margin-bottom:-3rem!important}.ml-sm-n5,.mx-sm-n5{margin-left:-3rem!important}.m-sm-auto{margin:auto!important}.mt-sm-auto,.my-sm-auto{margin-top:auto!important}.mr-sm-auto,.mx-sm-auto{margin-right:auto!important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto!important}.ml-sm-auto,.mx-sm-auto{margin-left:auto!important}}@media (min-width:768px){.m-md-0{margin:0!important}.mt-md-0,.my-md-0{margin-top:0!important}.mr-md-0,.mx-md-0{margin-right:0!important}.mb-md-0,.my-md-0{margin-bottom:0!important}.ml-md-0,.mx-md-0{margin-left:0!important}.m-md-1{margin:.25rem!important}.mt-md-1,.my-md-1{margin-top:.25rem!important}.mr-md-1,.mx-md-1{margin-right:.25rem!important}.mb-md-1,.my-md-1{margin-bottom:.25rem!important}.ml-md-1,.mx-md-1{margin-left:.25rem!important}.m-md-2{margin:.5rem!important}.mt-md-2,.my-md-2{margin-top:.5rem!important}.mr-md-2,.mx-md-2{margin-right:.5rem!important}.mb-md-2,.my-md-2{margin-bottom:.5rem!important}.ml-md-2,.mx-md-2{margin-left:.5rem!important}.m-md-3{margin:1rem!important}.mt-md-3,.my-md-3{margin-top:1rem!important}.mr-md-3,.mx-md-3{margin-right:1rem!important}.mb-md-3,.my-md-3{margin-bottom:1rem!important}.ml-md-3,.mx-md-3{margin-left:1rem!important}.m-md-4{margin:1.5rem!important}.mt-md-4,.my-md-4{margin-top:1.5rem!important}.mr-md-4,.mx-md-4{margin-right:1.5rem!important}.mb-md-4,.my-md-4{margin-bottom:1.5rem!important}.ml-md-4,.mx-md-4{margin-left:1.5rem!important}.m-md-5{margin:3rem!important}.mt-md-5,.my-md-5{margin-top:3rem!important}.mr-md-5,.mx-md-5{margin-right:3rem!important}.mb-md-5,.my-md-5{margin-bottom:3rem!important}.ml-md-5,.mx-md-5{margin-left:3rem!important}.p-md-0{padding:0!important}.pt-md-0,.py-md-0{padding-top:0!important}.pr-md-0,.px-md-0{padding-right:0!important}.pb-md-0,.py-md-0{padding-bottom:0!important}.pl-md-0,.px-md-0{padding-left:0!important}.p-md-1{padding:.25rem!important}.pt-md-1,.py-md-1{padding-top:.25rem!important}.pr-md-1,.px-md-1{padding-right:.25rem!important}.pb-md-1,.py-md-1{padding-bottom:.25rem!important}.pl-md-1,.px-md-1{padding-left:.25rem!important}.p-md-2{padding:.5rem!important}.pt-md-2,.py-md-2{padding-top:.5rem!important}.pr-md-2,.px-md-2{padding-right:.5rem!important}.pb-md-2,.py-md-2{padding-bottom:.5rem!important}.pl-md-2,.px-md-2{padding-left:.5rem!important}.p-md-3{padding:1rem!important}.pt-md-3,.py-md-3{padding-top:1rem!important}.pr-md-3,.px-md-3{padding-right:1rem!important}.pb-md-3,.py-md-3{padding-bottom:1rem!important}.pl-md-3,.px-md-3{padding-left:1rem!important}.p-md-4{padding:1.5rem!important}.pt-md-4,.py-md-4{padding-top:1.5rem!important}.pr-md-4,.px-md-4{padding-right:1.5rem!important}.pb-md-4,.py-md-4{padding-bottom:1.5rem!important}.pl-md-4,.px-md-4{padding-left:1.5rem!important}.p-md-5{padding:3rem!important}.pt-md-5,.py-md-5{padding-top:3rem!important}.pr-md-5,.px-md-5{padding-right:3rem!important}.pb-md-5,.py-md-5{padding-bottom:3rem!important}.pl-md-5,.px-md-5{padding-left:3rem!important}.m-md-n1{margin:-.25rem!important}.mt-md-n1,.my-md-n1{margin-top:-.25rem!important}.mr-md-n1,.mx-md-n1{margin-right:-.25rem!important}.mb-md-n1,.my-md-n1{margin-bottom:-.25rem!important}.ml-md-n1,.mx-md-n1{margin-left:-.25rem!important}.m-md-n2{margin:-.5rem!important}.mt-md-n2,.my-md-n2{margin-top:-.5rem!important}.mr-md-n2,.mx-md-n2{margin-right:-.5rem!important}.mb-md-n2,.my-md-n2{margin-bottom:-.5rem!important}.ml-md-n2,.mx-md-n2{margin-left:-.5rem!important}.m-md-n3{margin:-1rem!important}.mt-md-n3,.my-md-n3{margin-top:-1rem!important}.mr-md-n3,.mx-md-n3{margin-right:-1rem!important}.mb-md-n3,.my-md-n3{margin-bottom:-1rem!important}.ml-md-n3,.mx-md-n3{margin-left:-1rem!important}.m-md-n4{margin:-1.5rem!important}.mt-md-n4,.my-md-n4{margin-top:-1.5rem!important}.mr-md-n4,.mx-md-n4{margin-right:-1.5rem!important}.mb-md-n4,.my-md-n4{margin-bottom:-1.5rem!important}.ml-md-n4,.mx-md-n4{margin-left:-1.5rem!important}.m-md-n5{margin:-3rem!important}.mt-md-n5,.my-md-n5{margin-top:-3rem!important}.mr-md-n5,.mx-md-n5{margin-right:-3rem!important}.mb-md-n5,.my-md-n5{margin-bottom:-3rem!important}.ml-md-n5,.mx-md-n5{margin-left:-3rem!important}.m-md-auto{margin:auto!important}.mt-md-auto,.my-md-auto{margin-top:auto!important}.mr-md-auto,.mx-md-auto{margin-right:auto!important}.mb-md-auto,.my-md-auto{margin-bottom:auto!important}.ml-md-auto,.mx-md-auto{margin-left:auto!important}}@media (min-width:992px){.m-lg-0{margin:0!important}.mt-lg-0,.my-lg-0{margin-top:0!important}.mr-lg-0,.mx-lg-0{margin-right:0!important}.mb-lg-0,.my-lg-0{margin-bottom:0!important}.ml-lg-0,.mx-lg-0{margin-left:0!important}.m-lg-1{margin:.25rem!important}.mt-lg-1,.my-lg-1{margin-top:.25rem!important}.mr-lg-1,.mx-lg-1{margin-right:.25rem!important}.mb-lg-1,.my-lg-1{margin-bottom:.25rem!important}.ml-lg-1,.mx-lg-1{margin-left:.25rem!important}.m-lg-2{margin:.5rem!important}.mt-lg-2,.my-lg-2{margin-top:.5rem!important}.mr-lg-2,.mx-lg-2{margin-right:.5rem!important}.mb-lg-2,.my-lg-2{margin-bottom:.5rem!important}.ml-lg-2,.mx-lg-2{margin-left:.5rem!important}.m-lg-3{margin:1rem!important}.mt-lg-3,.my-lg-3{margin-top:1rem!important}.mr-lg-3,.mx-lg-3{margin-right:1rem!important}.mb-lg-3,.my-lg-3{margin-bottom:1rem!important}.ml-lg-3,.mx-lg-3{margin-left:1rem!important}.m-lg-4{margin:1.5rem!important}.mt-lg-4,.my-lg-4{margin-top:1.5rem!important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem!important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem!important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem!important}.m-lg-5{margin:3rem!important}.mt-lg-5,.my-lg-5{margin-top:3rem!important}.mr-lg-5,.mx-lg-5{margin-right:3rem!important}.mb-lg-5,.my-lg-5{margin-bottom:3rem!important}.ml-lg-5,.mx-lg-5{margin-left:3rem!important}.p-lg-0{padding:0!important}.pt-lg-0,.py-lg-0{padding-top:0!important}.pr-lg-0,.px-lg-0{padding-right:0!important}.pb-lg-0,.py-lg-0{padding-bottom:0!important}.pl-lg-0,.px-lg-0{padding-left:0!important}.p-lg-1{padding:.25rem!important}.pt-lg-1,.py-lg-1{padding-top:.25rem!important}.pr-lg-1,.px-lg-1{padding-right:.25rem!important}.pb-lg-1,.py-lg-1{padding-bottom:.25rem!important}.pl-lg-1,.px-lg-1{padding-left:.25rem!important}.p-lg-2{padding:.5rem!important}.pt-lg-2,.py-lg-2{padding-top:.5rem!important}.pr-lg-2,.px-lg-2{padding-right:.5rem!important}.pb-lg-2,.py-lg-2{padding-bottom:.5rem!important}.pl-lg-2,.px-lg-2{padding-left:.5rem!important}.p-lg-3{padding:1rem!important}.pt-lg-3,.py-lg-3{padding-top:1rem!important}.pr-lg-3,.px-lg-3{padding-right:1rem!important}.pb-lg-3,.py-lg-3{padding-bottom:1rem!important}.pl-lg-3,.px-lg-3{padding-left:1rem!important}.p-lg-4{padding:1.5rem!important}.pt-lg-4,.py-lg-4{padding-top:1.5rem!important}.pr-lg-4,.px-lg-4{padding-right:1.5rem!important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem!important}.pl-lg-4,.px-lg-4{padding-left:1.5rem!important}.p-lg-5{padding:3rem!important}.pt-lg-5,.py-lg-5{padding-top:3rem!important}.pr-lg-5,.px-lg-5{padding-right:3rem!important}.pb-lg-5,.py-lg-5{padding-bottom:3rem!important}.pl-lg-5,.px-lg-5{padding-left:3rem!important}.m-lg-n1{margin:-.25rem!important}.mt-lg-n1,.my-lg-n1{margin-top:-.25rem!important}.mr-lg-n1,.mx-lg-n1{margin-right:-.25rem!important}.mb-lg-n1,.my-lg-n1{margin-bottom:-.25rem!important}.ml-lg-n1,.mx-lg-n1{margin-left:-.25rem!important}.m-lg-n2{margin:-.5rem!important}.mt-lg-n2,.my-lg-n2{margin-top:-.5rem!important}.mr-lg-n2,.mx-lg-n2{margin-right:-.5rem!important}.mb-lg-n2,.my-lg-n2{margin-bottom:-.5rem!important}.ml-lg-n2,.mx-lg-n2{margin-left:-.5rem!important}.m-lg-n3{margin:-1rem!important}.mt-lg-n3,.my-lg-n3{margin-top:-1rem!important}.mr-lg-n3,.mx-lg-n3{margin-right:-1rem!important}.mb-lg-n3,.my-lg-n3{margin-bottom:-1rem!important}.ml-lg-n3,.mx-lg-n3{margin-left:-1rem!important}.m-lg-n4{margin:-1.5rem!important}.mt-lg-n4,.my-lg-n4{margin-top:-1.5rem!important}.mr-lg-n4,.mx-lg-n4{margin-right:-1.5rem!important}.mb-lg-n4,.my-lg-n4{margin-bottom:-1.5rem!important}.ml-lg-n4,.mx-lg-n4{margin-left:-1.5rem!important}.m-lg-n5{margin:-3rem!important}.mt-lg-n5,.my-lg-n5{margin-top:-3rem!important}.mr-lg-n5,.mx-lg-n5{margin-right:-3rem!important}.mb-lg-n5,.my-lg-n5{margin-bottom:-3rem!important}.ml-lg-n5,.mx-lg-n5{margin-left:-3rem!important}.m-lg-auto{margin:auto!important}.mt-lg-auto,.my-lg-auto{margin-top:auto!important}.mr-lg-auto,.mx-lg-auto{margin-right:auto!important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto!important}.ml-lg-auto,.mx-lg-auto{margin-left:auto!important}}@media (min-width:1200px){.m-xl-0{margin:0!important}.mt-xl-0,.my-xl-0{margin-top:0!important}.mr-xl-0,.mx-xl-0{margin-right:0!important}.mb-xl-0,.my-xl-0{margin-bottom:0!important}.ml-xl-0,.mx-xl-0{margin-left:0!important}.m-xl-1{margin:.25rem!important}.mt-xl-1,.my-xl-1{margin-top:.25rem!important}.mr-xl-1,.mx-xl-1{margin-right:.25rem!important}.mb-xl-1,.my-xl-1{margin-bottom:.25rem!important}.ml-xl-1,.mx-xl-1{margin-left:.25rem!important}.m-xl-2{margin:.5rem!important}.mt-xl-2,.my-xl-2{margin-top:.5rem!important}.mr-xl-2,.mx-xl-2{margin-right:.5rem!important}.mb-xl-2,.my-xl-2{margin-bottom:.5rem!important}.ml-xl-2,.mx-xl-2{margin-left:.5rem!important}.m-xl-3{margin:1rem!important}.mt-xl-3,.my-xl-3{margin-top:1rem!important}.mr-xl-3,.mx-xl-3{margin-right:1rem!important}.mb-xl-3,.my-xl-3{margin-bottom:1rem!important}.ml-xl-3,.mx-xl-3{margin-left:1rem!important}.m-xl-4{margin:1.5rem!important}.mt-xl-4,.my-xl-4{margin-top:1.5rem!important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem!important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem!important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem!important}.m-xl-5{margin:3rem!important}.mt-xl-5,.my-xl-5{margin-top:3rem!important}.mr-xl-5,.mx-xl-5{margin-right:3rem!important}.mb-xl-5,.my-xl-5{margin-bottom:3rem!important}.ml-xl-5,.mx-xl-5{margin-left:3rem!important}.p-xl-0{padding:0!important}.pt-xl-0,.py-xl-0{padding-top:0!important}.pr-xl-0,.px-xl-0{padding-right:0!important}.pb-xl-0,.py-xl-0{padding-bottom:0!important}.pl-xl-0,.px-xl-0{padding-left:0!important}.p-xl-1{padding:.25rem!important}.pt-xl-1,.py-xl-1{padding-top:.25rem!important}.pr-xl-1,.px-xl-1{padding-right:.25rem!important}.pb-xl-1,.py-xl-1{padding-bottom:.25rem!important}.pl-xl-1,.px-xl-1{padding-left:.25rem!important}.p-xl-2{padding:.5rem!important}.pt-xl-2,.py-xl-2{padding-top:.5rem!important}.pr-xl-2,.px-xl-2{padding-right:.5rem!important}.pb-xl-2,.py-xl-2{padding-bottom:.5rem!important}.pl-xl-2,.px-xl-2{padding-left:.5rem!important}.p-xl-3{padding:1rem!important}.pt-xl-3,.py-xl-3{padding-top:1rem!important}.pr-xl-3,.px-xl-3{padding-right:1rem!important}.pb-xl-3,.py-xl-3{padding-bottom:1rem!important}.pl-xl-3,.px-xl-3{padding-left:1rem!important}.p-xl-4{padding:1.5rem!important}.pt-xl-4,.py-xl-4{padding-top:1.5rem!important}.pr-xl-4,.px-xl-4{padding-right:1.5rem!important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem!important}.pl-xl-4,.px-xl-4{padding-left:1.5rem!important}.p-xl-5{padding:3rem!important}.pt-xl-5,.py-xl-5{padding-top:3rem!important}.pr-xl-5,.px-xl-5{padding-right:3rem!important}.pb-xl-5,.py-xl-5{padding-bottom:3rem!important}.pl-xl-5,.px-xl-5{padding-left:3rem!important}.m-xl-n1{margin:-.25rem!important}.mt-xl-n1,.my-xl-n1{margin-top:-.25rem!important}.mr-xl-n1,.mx-xl-n1{margin-right:-.25rem!important}.mb-xl-n1,.my-xl-n1{margin-bottom:-.25rem!important}.ml-xl-n1,.mx-xl-n1{margin-left:-.25rem!important}.m-xl-n2{margin:-.5rem!important}.mt-xl-n2,.my-xl-n2{margin-top:-.5rem!important}.mr-xl-n2,.mx-xl-n2{margin-right:-.5rem!important}.mb-xl-n2,.my-xl-n2{margin-bottom:-.5rem!important}.ml-xl-n2,.mx-xl-n2{margin-left:-.5rem!important}.m-xl-n3{margin:-1rem!important}.mt-xl-n3,.my-xl-n3{margin-top:-1rem!important}.mr-xl-n3,.mx-xl-n3{margin-right:-1rem!important}.mb-xl-n3,.my-xl-n3{margin-bottom:-1rem!important}.ml-xl-n3,.mx-xl-n3{margin-left:-1rem!important}.m-xl-n4{margin:-1.5rem!important}.mt-xl-n4,.my-xl-n4{margin-top:-1.5rem!important}.mr-xl-n4,.mx-xl-n4{margin-right:-1.5rem!important}.mb-xl-n4,.my-xl-n4{margin-bottom:-1.5rem!important}.ml-xl-n4,.mx-xl-n4{margin-left:-1.5rem!important}.m-xl-n5{margin:-3rem!important}.mt-xl-n5,.my-xl-n5{margin-top:-3rem!important}.mr-xl-n5,.mx-xl-n5{margin-right:-3rem!important}.mb-xl-n5,.my-xl-n5{margin-bottom:-3rem!important}.ml-xl-n5,.mx-xl-n5{margin-left:-3rem!important}.m-xl-auto{margin:auto!important}.mt-xl-auto,.my-xl-auto{margin-top:auto!important}.mr-xl-auto,.mx-xl-auto{margin-right:auto!important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto!important}.ml-xl-auto,.mx-xl-auto{margin-left:auto!important}}.text-monospace{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace!important}.text-justify{text-align:justify!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left!important}.text-right{text-align:right!important}.text-center{text-align:center!important}@media (min-width:576px){.text-sm-left{text-align:left!important}.text-sm-right{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.text-md-left{text-align:left!important}.text-md-right{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.text-lg-left{text-align:left!important}.text-lg-right{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.text-xl-left{text-align:left!important}.text-xl-right{text-align:right!important}.text-xl-center{text-align:center!important}}.text-lowercase{text-transform:lowercase!important}.dropdown .dropdown-menu .dropdown-header,.sidebar .sidebar-heading,.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.font-weight-light{font-weight:300!important}.font-weight-lighter{font-weight:lighter!important}.font-weight-normal{font-weight:400!important}.font-weight-bold{font-weight:700!important}.font-weight-bolder{font-weight:bolder!important}.font-italic{font-style:italic!important}.text-white{color:#fff!important}.text-primary{color:#4e73df!important}a.text-primary:focus,a.text-primary:hover{color:#224abe!important}.text-secondary{color:#858796!important}a.text-secondary:focus,a.text-secondary:hover{color:#60616f!important}.text-success{color:#1cc88a!important}a.text-success:focus,a.text-success:hover{color:#13855c!important}.text-info{color:#36b9cc!important}a.text-info:focus,a.text-info:hover{color:#258391!important}.text-warning{color:#f6c23e!important}a.text-warning:focus,a.text-warning:hover{color:#dda20a!important}.text-danger{color:#e74a3b!important}a.text-danger:focus,a.text-danger:hover{color:#be2617!important}.text-light{color:#f8f9fc!important}a.text-light:focus,a.text-light:hover{color:#c2cbe5!important}.text-dark{color:#5a5c69!important}a.text-dark:focus,a.text-dark:hover{color:#373840!important}.text-body{color:#858796!important}.text-muted{color:#858796!important}.text-black-50{color:rgba(0,0,0,.5)!important}.text-white-50{color:rgba(255,255,255,.5)!important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.text-decoration-none{text-decoration:none!important}.text-break{word-break:break-word!important;overflow-wrap:break-word!important}.text-reset{color:inherit!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media print{*,::after,::before{text-shadow:none!important;box-shadow:none!important}a:not(.btn){text-decoration:underline}abbr[title]::after{content:" (" attr(title) ")"}pre{white-space:pre-wrap!important}blockquote,pre{border:1px solid #b7b9cc;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}@page{size:a3}body{min-width:992px!important}.container{min-width:992px!important}.navbar{display:none}.badge{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #dddfeb!important}.table-dark{color:inherit}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#e3e6f0}.table .thead-dark th{color:inherit;border-color:#e3e6f0}}html{position:relative;min-height:100%}body{height:100%}a:focus{outline:0}#wrapper{display:flex}#wrapper #content-wrapper{background-color:#f8f9fc;width:100%;overflow-x:hidden}#wrapper #content-wrapper #content{flex:1 0 auto}.container,.container-fluid{padding-left:1.5rem;padding-right:1.5rem}.scroll-to-top{position:fixed;right:1rem;bottom:1rem;display:none;width:2.75rem;height:2.75rem;text-align:center;color:#fff;background:rgba(90,92,105,.5);line-height:46px}.scroll-to-top:focus,.scroll-to-top:hover{color:#fff}.scroll-to-top:hover{background:#5a5c69}.scroll-to-top i{font-weight:800}@-webkit-keyframes growIn{0%{transform:scale(.9);opacity:0}100%{transform:scale(1);opacity:1}}@keyframes growIn{0%{transform:scale(.9);opacity:0}100%{transform:scale(1);opacity:1}}.animated--grow-in,.sidebar .nav-item .collapse{-webkit-animation-name:growIn;animation-name:growIn;-webkit-animation-duration:.2s;animation-duration:.2s;-webkit-animation-timing-function:transform cubic-bezier(.18,1.25,.4,1),opacity cubic-bezier(0,1,.4,1);animation-timing-function:transform cubic-bezier(.18,1.25,.4,1),opacity cubic-bezier(0,1,.4,1)}@-webkit-keyframes fadeIn{0%{opacity:0}100%{opacity:1}}@keyframes fadeIn{0%{opacity:0}100%{opacity:1}}.animated--fade-in{-webkit-animation-name:fadeIn;animation-name:fadeIn;-webkit-animation-duration:.2s;animation-duration:.2s;-webkit-animation-timing-function:opacity cubic-bezier(0,1,.4,1);animation-timing-function:opacity cubic-bezier(0,1,.4,1)}.bg-gradient-primary{background-color:#4e73df;background-image:linear-gradient(180deg,#4e73df 10%,#224abe 100%);background-size:cover}.bg-gradient-secondary{background-color:#858796;background-image:linear-gradient(180deg,#858796 10%,#60616f 100%);background-size:cover}.bg-gradient-success{background-color:#1cc88a;background-image:linear-gradient(180deg,#1cc88a 10%,#13855c 100%);background-size:cover}.bg-gradient-info{background-color:#36b9cc;background-image:linear-gradient(180deg,#36b9cc 10%,#258391 100%);background-size:cover}.bg-gradient-warning{background-color:#f6c23e;background-image:linear-gradient(180deg,#f6c23e 10%,#dda20a 100%);background-size:cover}.bg-gradient-danger{background-color:#e74a3b;background-image:linear-gradient(180deg,#e74a3b 10%,#be2617 100%);background-size:cover}.bg-gradient-light{background-color:#f8f9fc;background-image:linear-gradient(180deg,#f8f9fc 10%,#c2cbe5 100%);background-size:cover}.bg-gradient-dark{background-color:#5a5c69;background-image:linear-gradient(180deg,#5a5c69 10%,#373840 100%);background-size:cover}.bg-gray-100{background-color:#f8f9fc!important}.bg-gray-200{background-color:#eaecf4!important}.bg-gray-300{background-color:#dddfeb!important}.bg-gray-400{background-color:#d1d3e2!important}.bg-gray-500{background-color:#b7b9cc!important}.bg-gray-600{background-color:#858796!important}.bg-gray-700{background-color:#6e707e!important}.bg-gray-800{background-color:#5a5c69!important}.bg-gray-900{background-color:#3a3b45!important}.o-hidden{overflow:hidden!important}.text-xs{font-size:.7rem}.text-lg{font-size:1.2rem}.text-gray-100{color:#f8f9fc!important}.text-gray-200{color:#eaecf4!important}.text-gray-300{color:#dddfeb!important}.text-gray-400{color:#d1d3e2!important}.text-gray-500{color:#b7b9cc!important}.text-gray-600{color:#858796!important}.text-gray-700{color:#6e707e!important}.text-gray-800{color:#5a5c69!important}.text-gray-900{color:#3a3b45!important}.icon-circle{height:2.5rem;width:2.5rem;border-radius:100%;display:flex;align-items:center;justify-content:center}.border-left-primary{border-left:.25rem solid #4e73df!important}.border-bottom-primary{border-bottom:.25rem solid #4e73df!important}.border-left-secondary{border-left:.25rem solid #858796!important}.border-bottom-secondary{border-bottom:.25rem solid #858796!important}.border-left-success{border-left:.25rem solid #1cc88a!important}.border-bottom-success{border-bottom:.25rem solid #1cc88a!important}.border-left-info{border-left:.25rem solid #36b9cc!important}.border-bottom-info{border-bottom:.25rem solid #36b9cc!important}.border-left-warning{border-left:.25rem solid #f6c23e!important}.border-bottom-warning{border-bottom:.25rem solid #f6c23e!important}.border-left-danger{border-left:.25rem solid #e74a3b!important}.border-bottom-danger{border-bottom:.25rem solid #e74a3b!important}.border-left-light{border-left:.25rem solid #f8f9fc!important}.border-bottom-light{border-bottom:.25rem solid #f8f9fc!important}.border-left-dark{border-left:.25rem solid #5a5c69!important}.border-bottom-dark{border-bottom:.25rem solid #5a5c69!important}.progress-sm{height:.5rem}.rotate-15{transform:rotate(15deg)}.rotate-n-15{transform:rotate(-15deg)}.dropdown .dropdown-menu{font-size:.85rem}.dropdown .dropdown-menu .dropdown-header{font-weight:800;font-size:.65rem;color:#b7b9cc}.dropdown.no-arrow .dropdown-toggle::after{display:none}.sidebar .nav-item.dropdown .dropdown-toggle::after,.topbar .nav-item.dropdown .dropdown-toggle::after{width:1rem;text-align:center;float:right;vertical-align:0;border:0;font-weight:900;content:'\f105';font-family:'Font Awesome 5 Free'}.sidebar .nav-item.dropdown.show .dropdown-toggle::after,.topbar .nav-item.dropdown.show .dropdown-toggle::after{content:'\f107'}.sidebar .nav-item .nav-link,.topbar .nav-item .nav-link{position:relative}.sidebar .nav-item .nav-link .badge-counter,.topbar .nav-item .nav-link .badge-counter{position:absolute;transform:scale(.7);transform-origin:top right;right:.25rem;margin-top:-.25rem}.sidebar .nav-item .nav-link .img-profile,.topbar .nav-item .nav-link .img-profile{height:2rem;width:2rem}.topbar{height:4.375rem}.topbar #sidebarToggleTop{height:2.5rem;width:2.5rem}.topbar #sidebarToggleTop:hover{background-color:#eaecf4}.topbar #sidebarToggleTop:active{background-color:#dddfeb}.topbar .navbar-search{width:25rem}.topbar .navbar-search input{font-size:.85rem;height:auto}.topbar .topbar-divider{width:0;border-right:1px solid #e3e6f0;height:calc(4.375rem - 2rem);margin:auto 1rem}.topbar .nav-item .nav-link{height:4.375rem;display:flex;align-items:center;padding:0 .75rem}.topbar .nav-item .nav-link:focus{outline:0}.topbar .nav-item:focus{outline:0}.topbar .dropdown{position:static}.topbar .dropdown .dropdown-menu{width:calc(100% - 1.5rem);right:.75rem}.topbar .dropdown-list{padding:0;border:none;overflow:hidden}.topbar .dropdown-list .dropdown-header{background-color:#4e73df;border:1px solid #4e73df;padding-top:.75rem;padding-bottom:.75rem;color:#fff}.topbar .dropdown-list .dropdown-item{white-space:normal;padding-top:.5rem;padding-bottom:.5rem;border-left:1px solid #e3e6f0;border-right:1px solid #e3e6f0;border-bottom:1px solid #e3e6f0;line-height:1.3rem}.topbar .dropdown-list .dropdown-item .dropdown-list-image{position:relative;height:2.5rem;width:2.5rem}.topbar .dropdown-list .dropdown-item .dropdown-list-image img{height:2.5rem;width:2.5rem}.topbar .dropdown-list .dropdown-item .dropdown-list-image .status-indicator{background-color:#eaecf4;height:.75rem;width:.75rem;border-radius:100%;position:absolute;bottom:0;right:0;border:.125rem solid #fff}.topbar .dropdown-list .dropdown-item .text-truncate{max-width:10rem}.topbar .dropdown-list .dropdown-item:active{background-color:#eaecf4;color:#3a3b45}@media (min-width:576px){.topbar .dropdown{position:relative}.topbar .dropdown .dropdown-menu{width:auto;right:0}.topbar .dropdown-list{width:20rem!important}.topbar .dropdown-list .dropdown-item .text-truncate{max-width:13.375rem}}.topbar.navbar-light .navbar-nav .nav-item .nav-link{color:#d1d3e2}.topbar.navbar-light .navbar-nav .nav-item .nav-link:hover{color:#b7b9cc}.topbar.navbar-light .navbar-nav .nav-item .nav-link:active{color:#858796}.sidebar{width:6.5rem;min-height:100vh}.sidebar .nav-item{position:relative}.sidebar .nav-item:last-child{margin-bottom:1rem}.sidebar .nav-item .nav-link{text-align:center;padding:.75rem 1rem;width:6.5rem}.sidebar .nav-item .nav-link span{font-size:.65rem;display:block}.sidebar .nav-item.active .nav-link{font-weight:700}.sidebar .nav-item .collapse{position:absolute;left:calc(6.5rem + 1.5rem / 2);z-index:1;top:2px}.sidebar .nav-item .collapse .collapse-inner{border-radius:.35rem;box-shadow:0 .15rem 1.75rem 0 rgba(58,59,69,.15)}.sidebar .nav-item .collapsing{display:none;transition:none}.sidebar .nav-item .collapse .collapse-inner,.sidebar .nav-item .collapsing .collapse-inner{padding:.5rem 0;min-width:10rem;font-size:.85rem;margin:0 0 1rem 0}.sidebar .nav-item .collapse .collapse-inner .collapse-header,.sidebar .nav-item .collapsing .collapse-inner .collapse-header{margin:0;white-space:nowrap;padding:.5rem 1.5rem;text-transform:uppercase;font-weight:800;font-size:.65rem;color:#b7b9cc}.sidebar .nav-item .collapse .collapse-inner .collapse-item,.sidebar .nav-item .collapsing .collapse-inner .collapse-item{padding:.5rem 1rem;margin:0 .5rem;display:block;color:#3a3b45;text-decoration:none;border-radius:.35rem;white-space:nowrap}.sidebar .nav-item .collapse .collapse-inner .collapse-item:hover,.sidebar .nav-item .collapsing .collapse-inner .collapse-item:hover{background-color:#eaecf4}.sidebar .nav-item .collapse .collapse-inner .collapse-item:active,.sidebar .nav-item .collapsing .collapse-inner .collapse-item:active{background-color:#dddfeb}.sidebar .nav-item .collapse .collapse-inner .collapse-item.active,.sidebar .nav-item .collapsing .collapse-inner .collapse-item.active{color:#4e73df;font-weight:700}.sidebar #sidebarToggle{width:2.5rem;height:2.5rem;text-align:center;margin-bottom:1rem;cursor:pointer}.sidebar #sidebarToggle::after{font-weight:900;content:'\f104';font-family:'Font Awesome 5 Free';margin-right:.1rem}.sidebar #sidebarToggle:hover{text-decoration:none}.sidebar #sidebarToggle:focus{outline:0}.sidebar.toggled{width:0!important;overflow:hidden}.sidebar.toggled #sidebarToggle::after{content:'\f105';font-family:'Font Awesome 5 Free';margin-left:.25rem}.sidebar .sidebar-brand{height:4.375rem;text-decoration:none;font-size:1rem;font-weight:800;padding:1.5rem 1rem;text-align:center;text-transform:uppercase;letter-spacing:.05rem;z-index:1}.sidebar .sidebar-brand .sidebar-brand-icon i{font-size:2rem}.sidebar .sidebar-brand .sidebar-brand-text{display:none}.sidebar hr.sidebar-divider{margin:0 1rem 1rem}.sidebar .sidebar-heading{text-align:center;padding:0 1rem;font-weight:800;font-size:.65rem}@media (min-width:768px){.sidebar{width:14rem!important}.sidebar .nav-item .collapse{position:relative;left:0;z-index:1;top:0;-webkit-animation:none;animation:none}.sidebar .nav-item .collapse .collapse-inner{border-radius:0;box-shadow:none}.sidebar .nav-item .collapsing{display:block;transition:height .15s ease}.sidebar .nav-item .collapse,.sidebar .nav-item .collapsing{margin:0 1rem}.sidebar .nav-item .nav-link{display:block;width:100%;text-align:left;padding:1rem;width:14rem}.sidebar .nav-item .nav-link i{font-size:.85rem;margin-right:.25rem}.sidebar .nav-item .nav-link span{font-size:.85rem;display:inline}.sidebar .nav-item .nav-link[data-toggle=collapse]::after{width:1rem;text-align:center;float:right;vertical-align:0;border:0;font-weight:900;content:'\f107';font-family:'Font Awesome 5 Free'}.sidebar .nav-item .nav-link[data-toggle=collapse].collapsed::after{content:'\f105'}.sidebar .sidebar-brand .sidebar-brand-icon i{font-size:2rem}.sidebar .sidebar-brand .sidebar-brand-text{display:inline}.sidebar .sidebar-heading{text-align:left}.sidebar.toggled{overflow:visible;width:6.5rem!important}.sidebar.toggled .nav-item .collapse{position:absolute;left:calc(6.5rem + 1.5rem / 2);z-index:1;top:2px;-webkit-animation-name:growIn;animation-name:growIn;-webkit-animation-duration:.2s;animation-duration:.2s;-webkit-animation-timing-function:transform cubic-bezier(.18,1.25,.4,1),opacity cubic-bezier(0,1,.4,1);animation-timing-function:transform cubic-bezier(.18,1.25,.4,1),opacity cubic-bezier(0,1,.4,1)}.sidebar.toggled .nav-item .collapse .collapse-inner{box-shadow:0 .15rem 1.75rem 0 rgba(58,59,69,.15);border-radius:.35rem}.sidebar.toggled .nav-item .collapsing{display:none;transition:none}.sidebar.toggled .nav-item .collapse,.sidebar.toggled .nav-item .collapsing{margin:0}.sidebar.toggled .nav-item:last-child{margin-bottom:1rem}.sidebar.toggled .nav-item .nav-link{text-align:center;padding:.75rem 1rem;width:6.5rem}.sidebar.toggled .nav-item .nav-link span{font-size:.65rem;display:block}.sidebar.toggled .nav-item .nav-link i{margin-right:0}.sidebar.toggled .nav-item .nav-link[data-toggle=collapse]::after{display:none}.sidebar.toggled .sidebar-brand .sidebar-brand-icon i{font-size:2rem}.sidebar.toggled .sidebar-brand .sidebar-brand-text{display:none}.sidebar.toggled .sidebar-heading{text-align:center}}.sidebar-light .sidebar-brand{color:#6e707e}.sidebar-light hr.sidebar-divider{border-top:1px solid #eaecf4}.sidebar-light .sidebar-heading{color:#b7b9cc}.sidebar-light .nav-item .nav-link{color:#858796}.sidebar-light .nav-item .nav-link i{color:#d1d3e2}.sidebar-light .nav-item .nav-link:active,.sidebar-light .nav-item .nav-link:focus,.sidebar-light .nav-item .nav-link:hover{color:#6e707e}.sidebar-light .nav-item .nav-link:active i,.sidebar-light .nav-item .nav-link:focus i,.sidebar-light .nav-item .nav-link:hover i{color:#6e707e}.sidebar-light .nav-item .nav-link[data-toggle=collapse]::after{color:#b7b9cc}.sidebar-light .nav-item.active .nav-link{color:#6e707e}.sidebar-light .nav-item.active .nav-link i{color:#6e707e}.sidebar-light #sidebarToggle{background-color:#eaecf4}.sidebar-light #sidebarToggle::after{color:#b7b9cc}.sidebar-light #sidebarToggle:hover{background-color:#dddfeb}.sidebar-dark .sidebar-brand{color:#fff}.sidebar-dark hr.sidebar-divider{border-top:1px solid rgba(255,255,255,.15)}.sidebar-dark .sidebar-heading{color:rgba(255,255,255,.4)}.sidebar-dark .nav-item .nav-link{color:rgba(255,255,255,.8)}.sidebar-dark .nav-item .nav-link i{color:rgba(255,255,255,.3)}.sidebar-dark .nav-item .nav-link:active,.sidebar-dark .nav-item .nav-link:focus,.sidebar-dark .nav-item .nav-link:hover{color:#fff}.sidebar-dark .nav-item .nav-link:active i,.sidebar-dark .nav-item .nav-link:focus i,.sidebar-dark .nav-item .nav-link:hover i{color:#fff}.sidebar-dark .nav-item .nav-link[data-toggle=collapse]::after{color:rgba(255,255,255,.5)}.sidebar-dark .nav-item.active .nav-link{color:#fff}.sidebar-dark .nav-item.active .nav-link i{color:#fff}.sidebar-dark #sidebarToggle{background-color:rgba(255,255,255,.2)}.sidebar-dark #sidebarToggle::after{color:rgba(255,255,255,.5)}.sidebar-dark #sidebarToggle:hover{background-color:rgba(255,255,255,.25)}.sidebar-dark.toggled #sidebarToggle::after{color:rgba(255,255,255,.5)}.btn-circle{border-radius:100%;height:2.5rem;width:2.5rem;font-size:1rem;display:inline-flex;align-items:center;justify-content:center}.btn-circle.btn-sm,.btn-group-sm>.btn-circle.btn{height:1.8rem;width:1.8rem;font-size:.75rem}.btn-circle.btn-lg,.btn-group-lg>.btn-circle.btn{height:3.5rem;width:3.5rem;font-size:1.35rem}.btn-icon-split{padding:0;overflow:hidden;display:inline-flex;align-items:stretch;justify-content:center}.btn-icon-split .icon{background:rgba(0,0,0,.15);display:inline-block;padding:.375rem .75rem}.btn-icon-split .text{display:inline-block;padding:.375rem .75rem}.btn-group-sm>.btn-icon-split.btn .icon,.btn-icon-split.btn-sm .icon{padding:.25rem .5rem}.btn-group-sm>.btn-icon-split.btn .text,.btn-icon-split.btn-sm .text{padding:.25rem .5rem}.btn-group-lg>.btn-icon-split.btn .icon,.btn-icon-split.btn-lg .icon{padding:.5rem 1rem}.btn-group-lg>.btn-icon-split.btn .text,.btn-icon-split.btn-lg .text{padding:.5rem 1rem}.card .card-header .dropdown{line-height:1}.card .card-header .dropdown .dropdown-menu{line-height:1.5}.card .card-header[data-toggle=collapse]{text-decoration:none;position:relative;padding:.75rem 3.25rem .75rem 1.25rem}.card .card-header[data-toggle=collapse]::after{position:absolute;right:0;top:0;padding-right:1.725rem;line-height:51px;font-weight:900;content:'\f107';font-family:'Font Awesome 5 Free';color:#d1d3e2}.card .card-header[data-toggle=collapse].collapsed{border-radius:.35rem}.card .card-header[data-toggle=collapse].collapsed::after{content:'\f105'}.chart-area{position:relative;height:10rem;width:100%}@media (min-width:768px){.chart-area{height:20rem}}.chart-bar{position:relative;height:10rem;width:100%}@media (min-width:768px){.chart-bar{height:20rem}}.chart-pie{position:relative;height:15rem;width:100%}@media (min-width:768px){.chart-pie{height:calc(20rem - 43px)!important}}.bg-login-image{background:url(https://source.unsplash.com/K4mSJ7kc0As/600x800);background-position:center;background-size:cover}.bg-register-image{background:url(https://source.unsplash.com/Mv9hjnEUHR4/600x800);background-position:center;background-size:cover}.bg-password-image{background:url(https://source.unsplash.com/oWTW-jNGl9I/600x800);background-position:center;background-size:cover}form.user .custom-checkbox.small label{line-height:1.5rem}form.user .form-control-user{font-size:.8rem;border-radius:10rem;padding:1.5rem 1rem}form.user .btn-user{font-size:.8rem;border-radius:10rem;padding:.75rem 1rem}.btn-google{color:#fff;background-color:#ea4335;border-color:#fff}.btn-google:hover{color:#fff;background-color:#e12717;border-color:#e6e6e6}.btn-google.focus,.btn-google:focus{box-shadow:0 0 0 .2rem rgba(255,255,255,.5)}.btn-google.disabled,.btn-google:disabled{color:#fff;background-color:#ea4335;border-color:#fff}.btn-google:not(:disabled):not(.disabled).active,.btn-google:not(:disabled):not(.disabled):active,.show>.btn-google.dropdown-toggle{color:#fff;background-color:#d62516;border-color:#dfdfdf}.btn-google:not(:disabled):not(.disabled).active:focus,.btn-google:not(:disabled):not(.disabled):active:focus,.show>.btn-google.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(255,255,255,.5)}.btn-facebook{color:#fff;background-color:#3b5998;border-color:#fff}.btn-facebook:hover{color:#fff;background-color:#30497c;border-color:#e6e6e6}.btn-facebook.focus,.btn-facebook:focus{box-shadow:0 0 0 .2rem rgba(255,255,255,.5)}.btn-facebook.disabled,.btn-facebook:disabled{color:#fff;background-color:#3b5998;border-color:#fff}.btn-facebook:not(:disabled):not(.disabled).active,.btn-facebook:not(:disabled):not(.disabled):active,.show>.btn-facebook.dropdown-toggle{color:#fff;background-color:#2d4373;border-color:#dfdfdf}.btn-facebook:not(:disabled):not(.disabled).active:focus,.btn-facebook:not(:disabled):not(.disabled):active:focus,.show>.btn-facebook.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(255,255,255,.5)}.error{color:#5a5c69;font-size:7rem;position:relative;line-height:1;width:12.5rem}@-webkit-keyframes noise-anim{0%{clip:rect(32px,9999px,16px,0)}5%{clip:rect(5px,9999px,24px,0)}10%{clip:rect(77px,9999px,87px,0)}15%{clip:rect(91px,9999px,95px,0)}20%{clip:rect(74px,9999px,9px,0)}25%{clip:rect(37px,9999px,32px,0)}30%{clip:rect(56px,9999px,27px,0)}35%{clip:rect(35px,9999px,33px,0)}40%{clip:rect(89px,9999px,6px,0)}45%{clip:rect(81px,9999px,77px,0)}50%{clip:rect(64px,9999px,69px,0)}55%{clip:rect(12px,9999px,11px,0)}60%{clip:rect(59px,9999px,11px,0)}65%{clip:rect(69px,9999px,59px,0)}70%{clip:rect(74px,9999px,65px,0)}75%{clip:rect(56px,9999px,79px,0)}80%{clip:rect(80px,9999px,64px,0)}85%{clip:rect(87px,9999px,29px,0)}90%{clip:rect(16px,9999px,21px,0)}95%{clip:rect(69px,9999px,43px,0)}100%{clip:rect(75px,9999px,63px,0)}}@keyframes noise-anim{0%{clip:rect(32px,9999px,16px,0)}5%{clip:rect(5px,9999px,24px,0)}10%{clip:rect(77px,9999px,87px,0)}15%{clip:rect(91px,9999px,95px,0)}20%{clip:rect(74px,9999px,9px,0)}25%{clip:rect(37px,9999px,32px,0)}30%{clip:rect(56px,9999px,27px,0)}35%{clip:rect(35px,9999px,33px,0)}40%{clip:rect(89px,9999px,6px,0)}45%{clip:rect(81px,9999px,77px,0)}50%{clip:rect(64px,9999px,69px,0)}55%{clip:rect(12px,9999px,11px,0)}60%{clip:rect(59px,9999px,11px,0)}65%{clip:rect(69px,9999px,59px,0)}70%{clip:rect(74px,9999px,65px,0)}75%{clip:rect(56px,9999px,79px,0)}80%{clip:rect(80px,9999px,64px,0)}85%{clip:rect(87px,9999px,29px,0)}90%{clip:rect(16px,9999px,21px,0)}95%{clip:rect(69px,9999px,43px,0)}100%{clip:rect(75px,9999px,63px,0)}}.error:after{content:attr(data-text);position:absolute;left:2px;text-shadow:-1px 0 #e74a3b;top:0;color:#5a5c69;background:#f8f9fc;overflow:hidden;clip:rect(0,900px,0,0);animation:noise-anim 2s infinite linear alternate-reverse}@-webkit-keyframes noise-anim-2{0%{clip:rect(12px,9999px,52px,0)}5%{clip:rect(42px,9999px,39px,0)}10%{clip:rect(64px,9999px,36px,0)}15%{clip:rect(52px,9999px,15px,0)}20%{clip:rect(79px,9999px,7px,0)}25%{clip:rect(17px,9999px,41px,0)}30%{clip:rect(15px,9999px,20px,0)}35%{clip:rect(62px,9999px,87px,0)}40%{clip:rect(94px,9999px,11px,0)}45%{clip:rect(49px,9999px,10px,0)}50%{clip:rect(82px,9999px,4px,0)}55%{clip:rect(70px,9999px,100px,0)}60%{clip:rect(62px,9999px,23px,0)}65%{clip:rect(51px,9999px,56px,0)}70%{clip:rect(41px,9999px,24px,0)}75%{clip:rect(6px,9999px,85px,0)}80%{clip:rect(96px,9999px,58px,0)}85%{clip:rect(16px,9999px,24px,0)}90%{clip:rect(40px,9999px,31px,0)}95%{clip:rect(91px,9999px,34px,0)}100%{clip:rect(87px,9999px,26px,0)}}@keyframes noise-anim-2{0%{clip:rect(12px,9999px,52px,0)}5%{clip:rect(42px,9999px,39px,0)}10%{clip:rect(64px,9999px,36px,0)}15%{clip:rect(52px,9999px,15px,0)}20%{clip:rect(79px,9999px,7px,0)}25%{clip:rect(17px,9999px,41px,0)}30%{clip:rect(15px,9999px,20px,0)}35%{clip:rect(62px,9999px,87px,0)}40%{clip:rect(94px,9999px,11px,0)}45%{clip:rect(49px,9999px,10px,0)}50%{clip:rect(82px,9999px,4px,0)}55%{clip:rect(70px,9999px,100px,0)}60%{clip:rect(62px,9999px,23px,0)}65%{clip:rect(51px,9999px,56px,0)}70%{clip:rect(41px,9999px,24px,0)}75%{clip:rect(6px,9999px,85px,0)}80%{clip:rect(96px,9999px,58px,0)}85%{clip:rect(16px,9999px,24px,0)}90%{clip:rect(40px,9999px,31px,0)}95%{clip:rect(91px,9999px,34px,0)}100%{clip:rect(87px,9999px,26px,0)}}.error:before{content:attr(data-text);position:absolute;left:-2px;text-shadow:1px 0 #4e73df;top:0;color:#5a5c69;background:#f8f9fc;overflow:hidden;clip:rect(0,900px,0,0);animation:noise-anim-2 3s infinite linear alternate-reverse}footer.sticky-footer{padding:2rem 0;flex-shrink:0}footer.sticky-footer .copyright{line-height:1;font-size:.8rem}body.sidebar-toggled footer.sticky-footer{width:100%} \ No newline at end of file diff --git a/static/favicon.ico b/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..43773ef1363ec86ab4ec9af5a54f234e5b9b8a42 GIT binary patch literal 4286 zcmeI0ze~eV5XWCyu};=WoSHN#C}waF2erK3>WZO5<`RUOzznqv*=$*#6S%e zTlfR?xrTn21V0>ioXKI$a=8dyMxFZN8rig~DVb zh9%~%z(?B}eU`hG@A$16eBWT3k{FU027bxw1=RRH7UWFOv!=TRb#+bobl~r{wx;Ln z|D3@HItNn*bxYr}{pZLB% z80I(1y!07z+9!^%_s`v4b4Q6YU7J>**A)m1qr$5w$OGvexw2AOq~!qqUicY9Tn9jw S=Mi=XY$%I)7FBWvB23?y%lL2r literal 0 HcmV?d00001 diff --git a/static/js/sb-admin-2.js b/static/js/sb-admin-2.js new file mode 100644 index 00000000..9e96613d --- /dev/null +++ b/static/js/sb-admin-2.js @@ -0,0 +1,49 @@ +(function($) { + "use strict"; // Start of use strict + + // Toggle the side navigation + $("#sidebarToggle, #sidebarToggleTop").on('click', function(e) { + $("body").toggleClass("sidebar-toggled"); + $(".sidebar").toggleClass("toggled"); + if ($(".sidebar").hasClass("toggled")) { + $('.sidebar .collapse').collapse('hide'); + }; + }); + + // Close any open menu accordions when window is resized below 768px + $(window).resize(function() { + if ($(window).width() < 768) { + $('.sidebar .collapse').collapse('hide'); + }; + }); + + // Prevent the content wrapper from scrolling when the fixed side navigation hovered over + $('body.fixed-nav .sidebar').on('mousewheel DOMMouseScroll wheel', function(e) { + if ($(window).width() > 768) { + var e0 = e.originalEvent, + delta = e0.wheelDelta || -e0.detail; + this.scrollTop += (delta < 0 ? 1 : -1) * 30; + e.preventDefault(); + } + }); + + // Scroll to top button appear + $(document).on('scroll', function() { + var scrollDistance = $(this).scrollTop(); + if (scrollDistance > 100) { + $('.scroll-to-top').fadeIn(); + } else { + $('.scroll-to-top').fadeOut(); + } + }); + + // Smooth scrolling using jQuery easing + $(document).on('click', 'a.scroll-to-top', function(e) { + var $anchor = $(this); + $('html, body').stop().animate({ + scrollTop: ($($anchor.attr('href')).offset().top) + }, 1000, 'easeInOutExpo'); + e.preventDefault(); + }); + +})(jQuery); // End of use strict diff --git a/static/js/sb-admin-2.min.js b/static/js/sb-admin-2.min.js new file mode 100644 index 00000000..595107e4 --- /dev/null +++ b/static/js/sb-admin-2.min.js @@ -0,0 +1,7 @@ +/*! + * Start Bootstrap - SB Admin 2 v4.0.7 (https://startbootstrap.com/template-overviews/sb-admin-2) + * Copyright 2013-2019 Start Bootstrap + * Licensed under MIT (https://github.com/BlackrockDigital/startbootstrap-sb-admin-2/blob/master/LICENSE) + */ + +!function(t){"use strict";t("#sidebarToggle, #sidebarToggleTop").on("click",function(o){t("body").toggleClass("sidebar-toggled"),t(".sidebar").toggleClass("toggled"),t(".sidebar").hasClass("toggled")&&t(".sidebar .collapse").collapse("hide")}),t(window).resize(function(){t(window).width()<768&&t(".sidebar .collapse").collapse("hide")}),t("body.fixed-nav .sidebar").on("mousewheel DOMMouseScroll wheel",function(o){if(768this._items.length-1||t<0))if(this._isSliding)p(this._element).one(q.SLID,function(){return e.to(t)});else{if(n===t)return this.pause(),void this.cycle();var i=n=i.clientWidth&&n>=i.clientHeight}),h=0l[t]&&!i.escapeWithReference&&(n=Math.min(h[e],l[t]-("right"===t?h.width:h.height))),Kt({},e,n)}};return c.forEach(function(t){var e=-1!==["left","top"].indexOf(t)?"primary":"secondary";h=Qt({},h,u[e](t))}),t.offsets.popper=h,t},priority:["left","right","top","bottom"],padding:5,boundariesElement:"scrollParent"},keepTogether:{order:400,enabled:!0,fn:function(t){var e=t.offsets,n=e.popper,i=e.reference,o=t.placement.split("-")[0],r=Math.floor,s=-1!==["top","bottom"].indexOf(o),a=s?"right":"bottom",l=s?"left":"top",c=s?"width":"height";return n[a]r(i[a])&&(t.offsets.popper[l]=r(i[a])),t}},arrow:{order:500,enabled:!0,fn:function(t,e){var n;if(!fe(t.instance.modifiers,"arrow","keepTogether"))return t;var i=e.element;if("string"==typeof i){if(!(i=t.instance.popper.querySelector(i)))return t}else if(!t.instance.popper.contains(i))return console.warn("WARNING: `arrow.element` must be child of its popper element!"),t;var o=t.placement.split("-")[0],r=t.offsets,s=r.popper,a=r.reference,l=-1!==["left","right"].indexOf(o),c=l?"height":"width",h=l?"Top":"Left",u=h.toLowerCase(),f=l?"left":"top",d=l?"bottom":"right",p=Zt(i)[c];a[d]-ps[d]&&(t.offsets.popper[u]+=a[u]+p-s[d]),t.offsets.popper=Vt(t.offsets.popper);var m=a[u]+a[c]/2-p/2,g=Nt(t.instance.popper),_=parseFloat(g["margin"+h],10),v=parseFloat(g["border"+h+"Width"],10),y=m-t.offsets.popper[u]-_-v;return y=Math.max(Math.min(s[c]-p,y),0),t.arrowElement=i,t.offsets.arrow=(Kt(n={},u,Math.round(y)),Kt(n,f,""),n),t},element:"[x-arrow]"},flip:{order:600,enabled:!0,fn:function(p,m){if(oe(p.instance.modifiers,"inner"))return p;if(p.flipped&&p.placement===p.originalPlacement)return p;var g=Gt(p.instance.popper,p.instance.reference,m.padding,m.boundariesElement,p.positionFixed),_=p.placement.split("-")[0],v=te(_),y=p.placement.split("-")[1]||"",E=[];switch(m.behavior){case ge:E=[_,v];break;case _e:E=me(_);break;case ve:E=me(_,!0);break;default:E=m.behavior}return E.forEach(function(t,e){if(_!==t||E.length===e+1)return p;_=p.placement.split("-")[0],v=te(_);var n,i=p.offsets.popper,o=p.offsets.reference,r=Math.floor,s="left"===_&&r(i.right)>r(o.left)||"right"===_&&r(i.left)r(o.top)||"bottom"===_&&r(i.top)r(g.right),c=r(i.top)r(g.bottom),u="left"===_&&a||"right"===_&&l||"top"===_&&c||"bottom"===_&&h,f=-1!==["top","bottom"].indexOf(_),d=!!m.flipVariations&&(f&&"start"===y&&a||f&&"end"===y&&l||!f&&"start"===y&&c||!f&&"end"===y&&h);(s||u||d)&&(p.flipped=!0,(s||u)&&(_=E[e+1]),d&&(y="end"===(n=y)?"start":"start"===n?"end":n),p.placement=_+(y?"-"+y:""),p.offsets.popper=Qt({},p.offsets.popper,ee(p.instance.popper,p.offsets.reference,p.placement)),p=ie(p.instance.modifiers,p,"flip"))}),p},behavior:"flip",padding:5,boundariesElement:"viewport"},inner:{order:700,enabled:!1,fn:function(t){var e=t.placement,n=e.split("-")[0],i=t.offsets,o=i.popper,r=i.reference,s=-1!==["left","right"].indexOf(n),a=-1===["top","left"].indexOf(n);return o[s?"left":"top"]=r[n]-(a?o[s?"width":"height"]:0),t.placement=te(e),t.offsets.popper=Vt(o),t}},hide:{order:800,enabled:!0,fn:function(t){if(!fe(t.instance.modifiers,"hide","preventOverflow"))return t;var e=t.offsets.reference,n=ne(t.instance.modifiers,function(t){return"preventOverflow"===t.name}).boundaries;if(e.bottomn.right||e.top>n.bottom||e.rightdocument.documentElement.clientHeight;!this._isBodyOverflowing&&t&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),this._isBodyOverflowing&&!t&&(this._element.style.paddingRight=this._scrollbarWidth+"px")},t._resetAdjustments=function(){this._element.style.paddingLeft="",this._element.style.paddingRight=""},t._checkScrollbar=function(){var t=document.body.getBoundingClientRect();this._isBodyOverflowing=t.left+t.right
',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:0,container:!1,fallbackPlacement:"flip",boundary:"scrollParent",sanitize:!0,sanitizeFn:null,whiteList:vn},Ln="show",xn="out",Pn={HIDE:"hide"+Tn,HIDDEN:"hidden"+Tn,SHOW:"show"+Tn,SHOWN:"shown"+Tn,INSERTED:"inserted"+Tn,CLICK:"click"+Tn,FOCUSIN:"focusin"+Tn,FOCUSOUT:"focusout"+Tn,MOUSEENTER:"mouseenter"+Tn,MOUSELEAVE:"mouseleave"+Tn},Hn="fade",jn="show",Rn=".tooltip-inner",Fn=".arrow",Mn="hover",Wn="focus",Un="click",Bn="manual",qn=function(){function i(t,e){if("undefined"==typeof be)throw new TypeError("Bootstrap's tooltips require Popper.js (https://popper.js.org/)");this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this.element=t,this.config=this._getConfig(e),this.tip=null,this._setListeners()}var t=i.prototype;return t.enable=function(){this._isEnabled=!0},t.disable=function(){this._isEnabled=!1},t.toggleEnabled=function(){this._isEnabled=!this._isEnabled},t.toggle=function(t){if(this._isEnabled)if(t){var e=this.constructor.DATA_KEY,n=p(t.currentTarget).data(e);n||(n=new this.constructor(t.currentTarget,this._getDelegateConfig()),p(t.currentTarget).data(e,n)),n._activeTrigger.click=!n._activeTrigger.click,n._isWithActiveTrigger()?n._enter(null,n):n._leave(null,n)}else{if(p(this.getTipElement()).hasClass(jn))return void this._leave(null,this);this._enter(null,this)}},t.dispose=function(){clearTimeout(this._timeout),p.removeData(this.element,this.constructor.DATA_KEY),p(this.element).off(this.constructor.EVENT_KEY),p(this.element).closest(".modal").off("hide.bs.modal"),this.tip&&p(this.tip).remove(),this._isEnabled=null,this._timeout=null,this._hoverState=null,(this._activeTrigger=null)!==this._popper&&this._popper.destroy(),this._popper=null,this.element=null,this.config=null,this.tip=null},t.show=function(){var e=this;if("none"===p(this.element).css("display"))throw new Error("Please use show on visible elements");var t=p.Event(this.constructor.Event.SHOW);if(this.isWithContent()&&this._isEnabled){p(this.element).trigger(t);var n=m.findShadowRoot(this.element),i=p.contains(null!==n?n:this.element.ownerDocument.documentElement,this.element);if(t.isDefaultPrevented()||!i)return;var o=this.getTipElement(),r=m.getUID(this.constructor.NAME);o.setAttribute("id",r),this.element.setAttribute("aria-describedby",r),this.setContent(),this.config.animation&&p(o).addClass(Hn);var s="function"==typeof this.config.placement?this.config.placement.call(this,o,this.element):this.config.placement,a=this._getAttachment(s);this.addAttachmentClass(a);var l=this._getContainer();p(o).data(this.constructor.DATA_KEY,this),p.contains(this.element.ownerDocument.documentElement,this.tip)||p(o).appendTo(l),p(this.element).trigger(this.constructor.Event.INSERTED),this._popper=new be(this.element,o,{placement:a,modifiers:{offset:this._getOffset(),flip:{behavior:this.config.fallbackPlacement},arrow:{element:Fn},preventOverflow:{boundariesElement:this.config.boundary}},onCreate:function(t){t.originalPlacement!==t.placement&&e._handlePopperPlacementChange(t)},onUpdate:function(t){return e._handlePopperPlacementChange(t)}}),p(o).addClass(jn),"ontouchstart"in document.documentElement&&p(document.body).children().on("mouseover",null,p.noop);var c=function(){e.config.animation&&e._fixTransition();var t=e._hoverState;e._hoverState=null,p(e.element).trigger(e.constructor.Event.SHOWN),t===xn&&e._leave(null,e)};if(p(this.tip).hasClass(Hn)){var h=m.getTransitionDurationFromElement(this.tip);p(this.tip).one(m.TRANSITION_END,c).emulateTransitionEnd(h)}else c()}},t.hide=function(t){var e=this,n=this.getTipElement(),i=p.Event(this.constructor.Event.HIDE),o=function(){e._hoverState!==Ln&&n.parentNode&&n.parentNode.removeChild(n),e._cleanTipClass(),e.element.removeAttribute("aria-describedby"),p(e.element).trigger(e.constructor.Event.HIDDEN),null!==e._popper&&e._popper.destroy(),t&&t()};if(p(this.element).trigger(i),!i.isDefaultPrevented()){if(p(n).removeClass(jn),"ontouchstart"in document.documentElement&&p(document.body).children().off("mouseover",null,p.noop),this._activeTrigger[Un]=!1,this._activeTrigger[Wn]=!1,this._activeTrigger[Mn]=!1,p(this.tip).hasClass(Hn)){var r=m.getTransitionDurationFromElement(n);p(n).one(m.TRANSITION_END,o).emulateTransitionEnd(r)}else o();this._hoverState=""}},t.update=function(){null!==this._popper&&this._popper.scheduleUpdate()},t.isWithContent=function(){return Boolean(this.getTitle())},t.addAttachmentClass=function(t){p(this.getTipElement()).addClass(Dn+"-"+t)},t.getTipElement=function(){return this.tip=this.tip||p(this.config.template)[0],this.tip},t.setContent=function(){var t=this.getTipElement();this.setElementContent(p(t.querySelectorAll(Rn)),this.getTitle()),p(t).removeClass(Hn+" "+jn)},t.setElementContent=function(t,e){"object"!=typeof e||!e.nodeType&&!e.jquery?this.config.html?(this.config.sanitize&&(e=bn(e,this.config.whiteList,this.config.sanitizeFn)),t.html(e)):t.text(e):this.config.html?p(e).parent().is(t)||t.empty().append(e):t.text(p(e).text())},t.getTitle=function(){var t=this.element.getAttribute("data-original-title");return t||(t="function"==typeof this.config.title?this.config.title.call(this.element):this.config.title),t},t._getOffset=function(){var e=this,t={};return"function"==typeof this.config.offset?t.fn=function(t){return t.offsets=l({},t.offsets,e.config.offset(t.offsets,e.element)||{}),t}:t.offset=this.config.offset,t},t._getContainer=function(){return!1===this.config.container?document.body:m.isElement(this.config.container)?p(this.config.container):p(document).find(this.config.container)},t._getAttachment=function(t){return Nn[t.toUpperCase()]},t._setListeners=function(){var i=this;this.config.trigger.split(" ").forEach(function(t){if("click"===t)p(i.element).on(i.constructor.Event.CLICK,i.config.selector,function(t){return i.toggle(t)});else if(t!==Bn){var e=t===Mn?i.constructor.Event.MOUSEENTER:i.constructor.Event.FOCUSIN,n=t===Mn?i.constructor.Event.MOUSELEAVE:i.constructor.Event.FOCUSOUT;p(i.element).on(e,i.config.selector,function(t){return i._enter(t)}).on(n,i.config.selector,function(t){return i._leave(t)})}}),p(this.element).closest(".modal").on("hide.bs.modal",function(){i.element&&i.hide()}),this.config.selector?this.config=l({},this.config,{trigger:"manual",selector:""}):this._fixTitle()},t._fixTitle=function(){var t=typeof this.element.getAttribute("data-original-title");(this.element.getAttribute("title")||"string"!==t)&&(this.element.setAttribute("data-original-title",this.element.getAttribute("title")||""),this.element.setAttribute("title",""))},t._enter=function(t,e){var n=this.constructor.DATA_KEY;(e=e||p(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),p(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusin"===t.type?Wn:Mn]=!0),p(e.getTipElement()).hasClass(jn)||e._hoverState===Ln?e._hoverState=Ln:(clearTimeout(e._timeout),e._hoverState=Ln,e.config.delay&&e.config.delay.show?e._timeout=setTimeout(function(){e._hoverState===Ln&&e.show()},e.config.delay.show):e.show())},t._leave=function(t,e){var n=this.constructor.DATA_KEY;(e=e||p(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),p(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusout"===t.type?Wn:Mn]=!1),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState=xn,e.config.delay&&e.config.delay.hide?e._timeout=setTimeout(function(){e._hoverState===xn&&e.hide()},e.config.delay.hide):e.hide())},t._isWithActiveTrigger=function(){for(var t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1},t._getConfig=function(t){var e=p(this.element).data();return Object.keys(e).forEach(function(t){-1!==An.indexOf(t)&&delete e[t]}),"number"==typeof(t=l({},this.constructor.Default,e,"object"==typeof t&&t?t:{})).delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),m.typeCheckConfig(wn,t,this.constructor.DefaultType),t.sanitize&&(t.template=bn(t.template,t.whiteList,t.sanitizeFn)),t},t._getDelegateConfig=function(){var t={};if(this.config)for(var e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t},t._cleanTipClass=function(){var t=p(this.getTipElement()),e=t.attr("class").match(In);null!==e&&e.length&&t.removeClass(e.join(""))},t._handlePopperPlacementChange=function(t){var e=t.instance;this.tip=e.popper,this._cleanTipClass(),this.addAttachmentClass(this._getAttachment(t.placement))},t._fixTransition=function(){var t=this.getTipElement(),e=this.config.animation;null===t.getAttribute("x-placement")&&(p(t).removeClass(Hn),this.config.animation=!1,this.hide(),this.show(),this.config.animation=e)},i._jQueryInterface=function(n){return this.each(function(){var t=p(this).data(Cn),e="object"==typeof n&&n;if((t||!/dispose|hide/.test(n))&&(t||(t=new i(this,e),p(this).data(Cn,t)),"string"==typeof n)){if("undefined"==typeof t[n])throw new TypeError('No method named "'+n+'"');t[n]()}})},s(i,null,[{key:"VERSION",get:function(){return"4.3.1"}},{key:"Default",get:function(){return kn}},{key:"NAME",get:function(){return wn}},{key:"DATA_KEY",get:function(){return Cn}},{key:"Event",get:function(){return Pn}},{key:"EVENT_KEY",get:function(){return Tn}},{key:"DefaultType",get:function(){return On}}]),i}();p.fn[wn]=qn._jQueryInterface,p.fn[wn].Constructor=qn,p.fn[wn].noConflict=function(){return p.fn[wn]=Sn,qn._jQueryInterface};var Kn="popover",Qn="bs.popover",Vn="."+Qn,Yn=p.fn[Kn],zn="bs-popover",Xn=new RegExp("(^|\\s)"+zn+"\\S+","g"),Gn=l({},qn.Default,{placement:"right",trigger:"click",content:"",template:''}),$n=l({},qn.DefaultType,{content:"(string|element|function)"}),Jn="fade",Zn="show",ti=".popover-header",ei=".popover-body",ni={HIDE:"hide"+Vn,HIDDEN:"hidden"+Vn,SHOW:"show"+Vn,SHOWN:"shown"+Vn,INSERTED:"inserted"+Vn,CLICK:"click"+Vn,FOCUSIN:"focusin"+Vn,FOCUSOUT:"focusout"+Vn,MOUSEENTER:"mouseenter"+Vn,MOUSELEAVE:"mouseleave"+Vn},ii=function(t){var e,n;function i(){return t.apply(this,arguments)||this}n=t,(e=i).prototype=Object.create(n.prototype),(e.prototype.constructor=e).__proto__=n;var o=i.prototype;return o.isWithContent=function(){return this.getTitle()||this._getContent()},o.addAttachmentClass=function(t){p(this.getTipElement()).addClass(zn+"-"+t)},o.getTipElement=function(){return this.tip=this.tip||p(this.config.template)[0],this.tip},o.setContent=function(){var t=p(this.getTipElement());this.setElementContent(t.find(ti),this.getTitle());var e=this._getContent();"function"==typeof e&&(e=e.call(this.element)),this.setElementContent(t.find(ei),e),t.removeClass(Jn+" "+Zn)},o._getContent=function(){return this.element.getAttribute("data-content")||this.config.content},o._cleanTipClass=function(){var t=p(this.getTipElement()),e=t.attr("class").match(Xn);null!==e&&0=this._offsets[o]&&("undefined"==typeof this._offsets[o+1]||tthis._items.length-1||t<0))if(this._isSliding)g(this._element).one(Q.SLID,function(){return e.to(t)});else{if(n===t)return this.pause(),void this.cycle();var i=ndocument.documentElement.clientHeight;!this._isBodyOverflowing&&t&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),this._isBodyOverflowing&&!t&&(this._element.style.paddingRight=this._scrollbarWidth+"px")},t._resetAdjustments=function(){this._element.style.paddingLeft="",this._element.style.paddingRight=""},t._checkScrollbar=function(){var t=document.body.getBoundingClientRect();this._isBodyOverflowing=t.left+t.right
',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:0,container:!1,fallbackPlacement:"flip",boundary:"scrollParent",sanitize:!0,sanitizeFn:null,whiteList:Ee},je="show",He="out",Re={HIDE:"hide"+De,HIDDEN:"hidden"+De,SHOW:"show"+De,SHOWN:"shown"+De,INSERTED:"inserted"+De,CLICK:"click"+De,FOCUSIN:"focusin"+De,FOCUSOUT:"focusout"+De,MOUSEENTER:"mouseenter"+De,MOUSELEAVE:"mouseleave"+De},xe="fade",Fe="show",Ue=".tooltip-inner",We=".arrow",qe="hover",Me="focus",Ke="click",Qe="manual",Be=function(){function i(t,e){if("undefined"==typeof u)throw new TypeError("Bootstrap's tooltips require Popper.js (https://popper.js.org/)");this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this.element=t,this.config=this._getConfig(e),this.tip=null,this._setListeners()}var t=i.prototype;return t.enable=function(){this._isEnabled=!0},t.disable=function(){this._isEnabled=!1},t.toggleEnabled=function(){this._isEnabled=!this._isEnabled},t.toggle=function(t){if(this._isEnabled)if(t){var e=this.constructor.DATA_KEY,n=g(t.currentTarget).data(e);n||(n=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(e,n)),n._activeTrigger.click=!n._activeTrigger.click,n._isWithActiveTrigger()?n._enter(null,n):n._leave(null,n)}else{if(g(this.getTipElement()).hasClass(Fe))return void this._leave(null,this);this._enter(null,this)}},t.dispose=function(){clearTimeout(this._timeout),g.removeData(this.element,this.constructor.DATA_KEY),g(this.element).off(this.constructor.EVENT_KEY),g(this.element).closest(".modal").off("hide.bs.modal"),this.tip&&g(this.tip).remove(),this._isEnabled=null,this._timeout=null,this._hoverState=null,(this._activeTrigger=null)!==this._popper&&this._popper.destroy(),this._popper=null,this.element=null,this.config=null,this.tip=null},t.show=function(){var e=this;if("none"===g(this.element).css("display"))throw new Error("Please use show on visible elements");var t=g.Event(this.constructor.Event.SHOW);if(this.isWithContent()&&this._isEnabled){g(this.element).trigger(t);var n=_.findShadowRoot(this.element),i=g.contains(null!==n?n:this.element.ownerDocument.documentElement,this.element);if(t.isDefaultPrevented()||!i)return;var o=this.getTipElement(),r=_.getUID(this.constructor.NAME);o.setAttribute("id",r),this.element.setAttribute("aria-describedby",r),this.setContent(),this.config.animation&&g(o).addClass(xe);var s="function"==typeof this.config.placement?this.config.placement.call(this,o,this.element):this.config.placement,a=this._getAttachment(s);this.addAttachmentClass(a);var l=this._getContainer();g(o).data(this.constructor.DATA_KEY,this),g.contains(this.element.ownerDocument.documentElement,this.tip)||g(o).appendTo(l),g(this.element).trigger(this.constructor.Event.INSERTED),this._popper=new u(this.element,o,{placement:a,modifiers:{offset:this._getOffset(),flip:{behavior:this.config.fallbackPlacement},arrow:{element:We},preventOverflow:{boundariesElement:this.config.boundary}},onCreate:function(t){t.originalPlacement!==t.placement&&e._handlePopperPlacementChange(t)},onUpdate:function(t){return e._handlePopperPlacementChange(t)}}),g(o).addClass(Fe),"ontouchstart"in document.documentElement&&g(document.body).children().on("mouseover",null,g.noop);var c=function(){e.config.animation&&e._fixTransition();var t=e._hoverState;e._hoverState=null,g(e.element).trigger(e.constructor.Event.SHOWN),t===He&&e._leave(null,e)};if(g(this.tip).hasClass(xe)){var h=_.getTransitionDurationFromElement(this.tip);g(this.tip).one(_.TRANSITION_END,c).emulateTransitionEnd(h)}else c()}},t.hide=function(t){var e=this,n=this.getTipElement(),i=g.Event(this.constructor.Event.HIDE),o=function(){e._hoverState!==je&&n.parentNode&&n.parentNode.removeChild(n),e._cleanTipClass(),e.element.removeAttribute("aria-describedby"),g(e.element).trigger(e.constructor.Event.HIDDEN),null!==e._popper&&e._popper.destroy(),t&&t()};if(g(this.element).trigger(i),!i.isDefaultPrevented()){if(g(n).removeClass(Fe),"ontouchstart"in document.documentElement&&g(document.body).children().off("mouseover",null,g.noop),this._activeTrigger[Ke]=!1,this._activeTrigger[Me]=!1,this._activeTrigger[qe]=!1,g(this.tip).hasClass(xe)){var r=_.getTransitionDurationFromElement(n);g(n).one(_.TRANSITION_END,o).emulateTransitionEnd(r)}else o();this._hoverState=""}},t.update=function(){null!==this._popper&&this._popper.scheduleUpdate()},t.isWithContent=function(){return Boolean(this.getTitle())},t.addAttachmentClass=function(t){g(this.getTipElement()).addClass(Ae+"-"+t)},t.getTipElement=function(){return this.tip=this.tip||g(this.config.template)[0],this.tip},t.setContent=function(){var t=this.getTipElement();this.setElementContent(g(t.querySelectorAll(Ue)),this.getTitle()),g(t).removeClass(xe+" "+Fe)},t.setElementContent=function(t,e){"object"!=typeof e||!e.nodeType&&!e.jquery?this.config.html?(this.config.sanitize&&(e=Se(e,this.config.whiteList,this.config.sanitizeFn)),t.html(e)):t.text(e):this.config.html?g(e).parent().is(t)||t.empty().append(e):t.text(g(e).text())},t.getTitle=function(){var t=this.element.getAttribute("data-original-title");return t||(t="function"==typeof this.config.title?this.config.title.call(this.element):this.config.title),t},t._getOffset=function(){var e=this,t={};return"function"==typeof this.config.offset?t.fn=function(t){return t.offsets=l({},t.offsets,e.config.offset(t.offsets,e.element)||{}),t}:t.offset=this.config.offset,t},t._getContainer=function(){return!1===this.config.container?document.body:_.isElement(this.config.container)?g(this.config.container):g(document).find(this.config.container)},t._getAttachment=function(t){return Pe[t.toUpperCase()]},t._setListeners=function(){var i=this;this.config.trigger.split(" ").forEach(function(t){if("click"===t)g(i.element).on(i.constructor.Event.CLICK,i.config.selector,function(t){return i.toggle(t)});else if(t!==Qe){var e=t===qe?i.constructor.Event.MOUSEENTER:i.constructor.Event.FOCUSIN,n=t===qe?i.constructor.Event.MOUSELEAVE:i.constructor.Event.FOCUSOUT;g(i.element).on(e,i.config.selector,function(t){return i._enter(t)}).on(n,i.config.selector,function(t){return i._leave(t)})}}),g(this.element).closest(".modal").on("hide.bs.modal",function(){i.element&&i.hide()}),this.config.selector?this.config=l({},this.config,{trigger:"manual",selector:""}):this._fixTitle()},t._fixTitle=function(){var t=typeof this.element.getAttribute("data-original-title");(this.element.getAttribute("title")||"string"!==t)&&(this.element.setAttribute("data-original-title",this.element.getAttribute("title")||""),this.element.setAttribute("title",""))},t._enter=function(t,e){var n=this.constructor.DATA_KEY;(e=e||g(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusin"===t.type?Me:qe]=!0),g(e.getTipElement()).hasClass(Fe)||e._hoverState===je?e._hoverState=je:(clearTimeout(e._timeout),e._hoverState=je,e.config.delay&&e.config.delay.show?e._timeout=setTimeout(function(){e._hoverState===je&&e.show()},e.config.delay.show):e.show())},t._leave=function(t,e){var n=this.constructor.DATA_KEY;(e=e||g(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusout"===t.type?Me:qe]=!1),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState=He,e.config.delay&&e.config.delay.hide?e._timeout=setTimeout(function(){e._hoverState===He&&e.hide()},e.config.delay.hide):e.hide())},t._isWithActiveTrigger=function(){for(var t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1},t._getConfig=function(t){var e=g(this.element).data();return Object.keys(e).forEach(function(t){-1!==Oe.indexOf(t)&&delete e[t]}),"number"==typeof(t=l({},this.constructor.Default,e,"object"==typeof t&&t?t:{})).delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),_.typeCheckConfig(be,t,this.constructor.DefaultType),t.sanitize&&(t.template=Se(t.template,t.whiteList,t.sanitizeFn)),t},t._getDelegateConfig=function(){var t={};if(this.config)for(var e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t},t._cleanTipClass=function(){var t=g(this.getTipElement()),e=t.attr("class").match(Ne);null!==e&&e.length&&t.removeClass(e.join(""))},t._handlePopperPlacementChange=function(t){var e=t.instance;this.tip=e.popper,this._cleanTipClass(),this.addAttachmentClass(this._getAttachment(t.placement))},t._fixTransition=function(){var t=this.getTipElement(),e=this.config.animation;null===t.getAttribute("x-placement")&&(g(t).removeClass(xe),this.config.animation=!1,this.hide(),this.show(),this.config.animation=e)},i._jQueryInterface=function(n){return this.each(function(){var t=g(this).data(Ie),e="object"==typeof n&&n;if((t||!/dispose|hide/.test(n))&&(t||(t=new i(this,e),g(this).data(Ie,t)),"string"==typeof n)){if("undefined"==typeof t[n])throw new TypeError('No method named "'+n+'"');t[n]()}})},s(i,null,[{key:"VERSION",get:function(){return"4.3.1"}},{key:"Default",get:function(){return Le}},{key:"NAME",get:function(){return be}},{key:"DATA_KEY",get:function(){return Ie}},{key:"Event",get:function(){return Re}},{key:"EVENT_KEY",get:function(){return De}},{key:"DefaultType",get:function(){return ke}}]),i}();g.fn[be]=Be._jQueryInterface,g.fn[be].Constructor=Be,g.fn[be].noConflict=function(){return g.fn[be]=we,Be._jQueryInterface};var Ve="popover",Ye="bs.popover",ze="."+Ye,Xe=g.fn[Ve],$e="bs-popover",Ge=new RegExp("(^|\\s)"+$e+"\\S+","g"),Je=l({},Be.Default,{placement:"right",trigger:"click",content:"",template:''}),Ze=l({},Be.DefaultType,{content:"(string|element|function)"}),tn="fade",en="show",nn=".popover-header",on=".popover-body",rn={HIDE:"hide"+ze,HIDDEN:"hidden"+ze,SHOW:"show"+ze,SHOWN:"shown"+ze,INSERTED:"inserted"+ze,CLICK:"click"+ze,FOCUSIN:"focusin"+ze,FOCUSOUT:"focusout"+ze,MOUSEENTER:"mouseenter"+ze,MOUSELEAVE:"mouseleave"+ze},sn=function(t){var e,n;function i(){return t.apply(this,arguments)||this}n=t,(e=i).prototype=Object.create(n.prototype),(e.prototype.constructor=e).__proto__=n;var o=i.prototype;return o.isWithContent=function(){return this.getTitle()||this._getContent()},o.addAttachmentClass=function(t){g(this.getTipElement()).addClass($e+"-"+t)},o.getTipElement=function(){return this.tip=this.tip||g(this.config.template)[0],this.tip},o.setContent=function(){var t=g(this.getTipElement());this.setElementContent(t.find(nn),this.getTitle());var e=this._getContent();"function"==typeof e&&(e=e.call(this.element)),this.setElementContent(t.find(on),e),t.removeClass(tn+" "+en)},o._getContent=function(){return this.element.getAttribute("data-content")||this.config.content},o._cleanTipClass=function(){var t=g(this.getTipElement()),e=t.attr("class").match(Ge);null!==e&&0=this._offsets[o]&&("undefined"==typeof this._offsets[o+1]||tdiv{padding:1em}div.dt-button-collection-title{text-align:center;padding:0.3em 0 0.5em;font-size:0.9em}div.dt-button-collection-title:empty{display:none}div.dt-button-collection.dropdown-menu{display:block;z-index:2002}div.dt-button-collection.dropdown-menu.fixed{position:fixed;top:50%;left:50%;margin-left:-75px;border-radius:0}div.dt-button-collection.dropdown-menu.fixed.two-column{margin-left:-150px}div.dt-button-collection.dropdown-menu.fixed.three-column{margin-left:-225px}div.dt-button-collection.dropdown-menu.fixed.four-column{margin-left:-300px}div.dt-button-collection.dropdown-menu>div:last-child{-webkit-column-gap:8px;-moz-column-gap:8px;-ms-column-gap:8px;-o-column-gap:8px;column-gap:8px}div.dt-button-collection.dropdown-menu>div:last-child>*{-webkit-column-break-inside:avoid;break-inside:avoid}div.dt-button-collection.dropdown-menu.two-column{width:300px}div.dt-button-collection.dropdown-menu.two-column>div:last-child{padding-bottom:1px;-webkit-column-count:2;-moz-column-count:2;-ms-column-count:2;-o-column-count:2;column-count:2}div.dt-button-collection.dropdown-menu.three-column{width:450px}div.dt-button-collection.dropdown-menu.three-column>div:last-child{padding-bottom:1px;-webkit-column-count:3;-moz-column-count:3;-ms-column-count:3;-o-column-count:3;column-count:3}div.dt-button-collection.dropdown-menu.four-column{width:600px}div.dt-button-collection.dropdown-menu.four-column>div:last-child{padding-bottom:1px;-webkit-column-count:4;-moz-column-count:4;-ms-column-count:4;-o-column-count:4;column-count:4}div.dt-button-collection.dropdown-menu .dt-button{border-radius:0}div.dt-button-collection.fixed{position:fixed;top:50%;left:50%;margin-left:-75px;border-radius:0}div.dt-button-collection.fixed.two-column{margin-left:-150px}div.dt-button-collection.fixed.three-column{margin-left:-225px}div.dt-button-collection.fixed.four-column{margin-left:-300px}div.dt-button-collection>div:last-child{-webkit-column-gap:8px;-moz-column-gap:8px;-ms-column-gap:8px;-o-column-gap:8px;column-gap:8px}div.dt-button-collection>div:last-child>*{-webkit-column-break-inside:avoid;break-inside:avoid}div.dt-button-collection.two-column{width:300px}div.dt-button-collection.two-column>div:last-child{padding-bottom:1px;-webkit-column-count:2;-moz-column-count:2;-ms-column-count:2;-o-column-count:2;column-count:2}div.dt-button-collection.three-column{width:450px}div.dt-button-collection.three-column>div:last-child{padding-bottom:1px;-webkit-column-count:3;-moz-column-count:3;-ms-column-count:3;-o-column-count:3;column-count:3}div.dt-button-collection.four-column{width:600px}div.dt-button-collection.four-column>div:last-child{padding-bottom:1px;-webkit-column-count:4;-moz-column-count:4;-ms-column-count:4;-o-column-count:4;column-count:4}div.dt-button-collection .dt-button{border-radius:0}div.dt-button-collection.fixed{max-width:none}div.dt-button-collection.fixed:before,div.dt-button-collection.fixed:after{display:none}div.dt-button-background{position:fixed;top:0;left:0;width:100%;height:100%;z-index:999}@media screen and (max-width: 767px){div.dt-buttons{float:none;width:100%;text-align:center;margin-bottom:0.5em}div.dt-buttons a.btn{float:none}}div.dt-buttons button.btn.processing,div.dt-buttons div.btn.processing,div.dt-buttons a.btn.processing{color:rgba(0,0,0,0.2)}div.dt-buttons button.btn.processing:after,div.dt-buttons div.btn.processing:after,div.dt-buttons a.btn.processing:after{position:absolute;top:50%;left:50%;width:16px;height:16px;margin:-8px 0 0 -8px;box-sizing:border-box;display:block;content:' ';border:2px solid #282828;border-radius:50%;border-left-color:transparent;border-right-color:transparent;animation:dtb-spinner 1500ms infinite linear;-o-animation:dtb-spinner 1500ms infinite linear;-ms-animation:dtb-spinner 1500ms infinite linear;-webkit-animation:dtb-spinner 1500ms infinite linear;-moz-animation:dtb-spinner 1500ms infinite linear} diff --git a/static/vendor/datatables/buttons.bootstrap4.min.js b/static/vendor/datatables/buttons.bootstrap4.min.js new file mode 100644 index 00000000..10d7cfd2 --- /dev/null +++ b/static/vendor/datatables/buttons.bootstrap4.min.js @@ -0,0 +1,6 @@ +/*! + Bootstrap integration for DataTables' Buttons + ©2016 SpryMedia Ltd - datatables.net/license +*/ +(function(c){"function"===typeof define&&define.amd?define(["jquery","datatables.net-bs4","datatables.net-buttons"],function(a){return c(a,window,document)}):"object"===typeof exports?module.exports=function(a,b){a||(a=window);b&&b.fn.dataTable||(b=require("datatables.net-bs4")(a,b).$);b.fn.dataTable.Buttons||require("datatables.net-buttons")(a,b);return c(b,a,a.document)}:c(jQuery,window,document)})(function(c,a,b,d){a=c.fn.dataTable;c.extend(!0,a.Buttons.defaults,{dom:{container:{className:"dt-buttons btn-group flex-wrap"}, +button:{className:"btn btn-secondary"},collection:{tag:"div",className:"dt-button-collection dropdown-menu",button:{tag:"a",className:"dt-button dropdown-item",active:"active",disabled:"disabled"}}},buttonCreated:function(a,b){return a.buttons?c('
').append(b):b}});a.ext.buttons.collection.className+=" dropdown-toggle";a.ext.buttons.collection.rightAlignClassName="dropdown-menu-right";return a.Buttons}); diff --git a/static/vendor/datatables/dataTables.bootstrap4.min.css b/static/vendor/datatables/dataTables.bootstrap4.min.css new file mode 100644 index 00000000..f1930be0 --- /dev/null +++ b/static/vendor/datatables/dataTables.bootstrap4.min.css @@ -0,0 +1 @@ +table.dataTable{clear:both;margin-top:6px !important;margin-bottom:6px !important;max-width:none !important;border-collapse:separate !important;border-spacing:0}table.dataTable td,table.dataTable th{-webkit-box-sizing:content-box;box-sizing:content-box}table.dataTable td.dataTables_empty,table.dataTable th.dataTables_empty{text-align:center}table.dataTable.nowrap th,table.dataTable.nowrap td{white-space:nowrap}div.dataTables_wrapper div.dataTables_length label{font-weight:normal;text-align:left;white-space:nowrap}div.dataTables_wrapper div.dataTables_length select{width:auto;display:inline-block}div.dataTables_wrapper div.dataTables_filter{text-align:right}div.dataTables_wrapper div.dataTables_filter label{font-weight:normal;white-space:nowrap;text-align:left}div.dataTables_wrapper div.dataTables_filter input{margin-left:0.5em;display:inline-block;width:auto}div.dataTables_wrapper div.dataTables_info{padding-top:0.85em;white-space:nowrap}div.dataTables_wrapper div.dataTables_paginate{margin:0;white-space:nowrap;text-align:right}div.dataTables_wrapper div.dataTables_paginate ul.pagination{margin:2px 0;white-space:nowrap;justify-content:flex-end}div.dataTables_wrapper div.dataTables_processing{position:absolute;top:50%;left:50%;width:200px;margin-left:-100px;margin-top:-26px;text-align:center;padding:1em 0}table.dataTable thead>tr>th.sorting_asc,table.dataTable thead>tr>th.sorting_desc,table.dataTable thead>tr>th.sorting,table.dataTable thead>tr>td.sorting_asc,table.dataTable thead>tr>td.sorting_desc,table.dataTable thead>tr>td.sorting{padding-right:30px}table.dataTable thead>tr>th:active,table.dataTable thead>tr>td:active{outline:none}table.dataTable thead .sorting,table.dataTable thead .sorting_asc,table.dataTable thead .sorting_desc,table.dataTable thead .sorting_asc_disabled,table.dataTable thead .sorting_desc_disabled{cursor:pointer;position:relative}table.dataTable thead .sorting:before,table.dataTable thead .sorting:after,table.dataTable thead .sorting_asc:before,table.dataTable thead .sorting_asc:after,table.dataTable thead .sorting_desc:before,table.dataTable thead .sorting_desc:after,table.dataTable thead .sorting_asc_disabled:before,table.dataTable thead .sorting_asc_disabled:after,table.dataTable thead .sorting_desc_disabled:before,table.dataTable thead .sorting_desc_disabled:after{position:absolute;bottom:0.9em;display:block;opacity:0.3}table.dataTable thead .sorting:before,table.dataTable thead .sorting_asc:before,table.dataTable thead .sorting_desc:before,table.dataTable thead .sorting_asc_disabled:before,table.dataTable thead .sorting_desc_disabled:before{right:1em;content:"\2191"}table.dataTable thead .sorting:after,table.dataTable thead .sorting_asc:after,table.dataTable thead .sorting_desc:after,table.dataTable thead .sorting_asc_disabled:after,table.dataTable thead .sorting_desc_disabled:after{right:0.5em;content:"\2193"}table.dataTable thead .sorting_asc:before,table.dataTable thead .sorting_desc:after{opacity:1}table.dataTable thead .sorting_asc_disabled:before,table.dataTable thead .sorting_desc_disabled:after{opacity:0}div.dataTables_scrollHead table.dataTable{margin-bottom:0 !important}div.dataTables_scrollBody table{border-top:none;margin-top:0 !important;margin-bottom:0 !important}div.dataTables_scrollBody table thead .sorting:before,div.dataTables_scrollBody table thead .sorting_asc:before,div.dataTables_scrollBody table thead .sorting_desc:before,div.dataTables_scrollBody table thead .sorting:after,div.dataTables_scrollBody table thead .sorting_asc:after,div.dataTables_scrollBody table thead .sorting_desc:after{display:none}div.dataTables_scrollBody table tbody tr:first-child th,div.dataTables_scrollBody table tbody tr:first-child td{border-top:none}div.dataTables_scrollFoot>.dataTables_scrollFootInner{box-sizing:content-box}div.dataTables_scrollFoot>.dataTables_scrollFootInner>table{margin-top:0 !important;border-top:none}@media screen and (max-width: 767px){div.dataTables_wrapper div.dataTables_length,div.dataTables_wrapper div.dataTables_filter,div.dataTables_wrapper div.dataTables_info,div.dataTables_wrapper div.dataTables_paginate{text-align:center}}table.dataTable.table-sm>thead>tr>th{padding-right:20px}table.dataTable.table-sm .sorting:before,table.dataTable.table-sm .sorting_asc:before,table.dataTable.table-sm .sorting_desc:before{top:5px;right:0.85em}table.dataTable.table-sm .sorting:after,table.dataTable.table-sm .sorting_asc:after,table.dataTable.table-sm .sorting_desc:after{top:5px}table.table-bordered.dataTable th,table.table-bordered.dataTable td{border-left-width:0}table.table-bordered.dataTable th:last-child,table.table-bordered.dataTable th:last-child,table.table-bordered.dataTable td:last-child,table.table-bordered.dataTable td:last-child{border-right-width:0}table.table-bordered.dataTable tbody th,table.table-bordered.dataTable tbody td{border-bottom-width:0}div.dataTables_scrollHead table.table-bordered{border-bottom-width:0}div.table-responsive>div.dataTables_wrapper>div.row{margin:0}div.table-responsive>div.dataTables_wrapper>div.row>div[class^="col-"]:first-child{padding-left:0}div.table-responsive>div.dataTables_wrapper>div.row>div[class^="col-"]:last-child{padding-right:0} diff --git a/static/vendor/datatables/dataTables.bootstrap4.min.js b/static/vendor/datatables/dataTables.bootstrap4.min.js new file mode 100644 index 00000000..8cecad73 --- /dev/null +++ b/static/vendor/datatables/dataTables.bootstrap4.min.js @@ -0,0 +1,11 @@ +/*! + DataTables Bootstrap 4 integration + ©2011-2017 SpryMedia Ltd - datatables.net/license +*/ +var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.findInternal=function(a,b,c){a instanceof String&&(a=String(a));for(var e=a.length,d=0;d<'col-sm-12 col-md-6'f>><'row'<'col-sm-12'tr>><'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>", +renderer:"bootstrap"});a.extend(d.ext.classes,{sWrapper:"dataTables_wrapper dt-bootstrap4",sFilterInput:"form-control form-control-sm",sLengthSelect:"custom-select custom-select-sm form-control form-control-sm",sProcessing:"dataTables_processing card",sPageButton:"paginate_button page-item"});d.ext.renderer.pageButton.bootstrap=function(b,l,v,w,m,r){var k=new d.Api(b),x=b.oClasses,n=b.oLanguage.oPaginate,y=b.oLanguage.oAria.paginate||{},g,h,t=0,u=function(c,d){var e,l=function(b){b.preventDefault(); +a(b.currentTarget).hasClass("disabled")||k.page()==b.data.action||k.page(b.data.action).draw("page")};var q=0;for(e=d.length;q",{"class":x.sPageButton+" "+h,id:0===v&&"string"===typeof f?b.sTableId+"_"+f:null}).append(a("",{href:"#","aria-controls":b.sTableId,"aria-label":y[f],"data-dt-idx":t,tabindex:b.iTabIndex,"class":"page-link"}).html(g)).appendTo(c);b.oApi._fnBindAction(p,{action:f},l);t++}}}};try{var p=a(l).find(c.activeElement).data("dt-idx")}catch(z){}u(a(l).empty().html('