From e269fc8c0a880c9ecae11e0c9f7b7712e5f1acf1 Mon Sep 17 00:00:00 2001 From: Graham Steffaniak <42989099+gtsteffaniak@users.noreply.github.com> Date: Fri, 31 Jan 2025 15:26:21 -0500 Subject: [PATCH] Beta/v0.5.0 (#333) --- .github/workflows/main.yaml | 4 +- .github/workflows/pr.yaml | 4 +- .github/workflows/release_beta.yaml | 4 +- .github/workflows/release_dev.yaml | 2 +- .github/workflows/release_stable.yaml | 4 +- CHANGELOG.md | 32 ++++- README.md | 22 +-- Dockerfile => _docker/Dockerfile | 0 .../Dockerfile.playwright | 0 .../Dockerfile.playwright-base | 0 _docker/docker-compose.yaml | 15 ++ _docker/src/config.yaml | 8 ++ _docker/src/default.conf | 13 ++ backend/auth/storage.go | 21 ++- backend/cmd/root.go | 54 +++++++- backend/config.yaml | 1 - backend/http/auth.go | 7 +- backend/http/middleware.go | 34 ++++- backend/http/router.go | 119 +++++++++------- backend/http/static.go | 10 +- backend/settings/config.go | 20 ++- backend/settings/settings_test.go | 2 - backend/settings/structs.go | 41 ++++-- backend/settings/testingConfig.yaml | 1 - backend/storage/bolt/bolt.go | 5 +- backend/storage/bolt/users.go | 4 +- backend/storage/storage.go | 8 -- backend/swagger/docs/docs.go | 20 ++- backend/swagger/docs/swagger.json | 20 ++- backend/swagger/docs/swagger.yaml | 14 +- frontend/playwright.config.ts | 2 +- frontend/public/index.html | 5 + frontend/src/api/files.js | 9 +- frontend/src/components/ContextMenu.vue | 2 +- frontend/src/components/Notifications.vue | 29 ++++ frontend/src/components/settings/UserForm.vue | 8 +- frontend/src/components/sidebar/General.vue | 4 +- frontend/src/components/sidebar/Sidebar.vue | 2 +- frontend/src/css/listing.css | 2 +- frontend/src/notify/index.ts | 12 +- frontend/src/notify/loadingSpinner.js | 87 ++++++++++++ frontend/src/notify/message.js | 130 ++++++++++-------- frontend/src/store/state.js | 2 +- frontend/src/utils/constants.js | 2 - frontend/src/utils/filesizes.test.js | 21 +++ frontend/src/utils/upload.js | 41 +++++- frontend/src/views/Layout.vue | 14 +- frontend/src/views/Login.vue | 4 +- frontend/src/views/files/ListingView.vue | 2 +- makefile | 4 +- 50 files changed, 644 insertions(+), 227 deletions(-) rename Dockerfile => _docker/Dockerfile (100%) rename Dockerfile.playwright => _docker/Dockerfile.playwright (100%) rename Dockerfile.playwright-base => _docker/Dockerfile.playwright-base (100%) create mode 100644 _docker/docker-compose.yaml create mode 100644 _docker/src/config.yaml create mode 100644 _docker/src/default.conf create mode 100644 frontend/src/components/Notifications.vue create mode 100644 frontend/src/notify/loadingSpinner.js create mode 100644 frontend/src/utils/filesizes.test.js diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 7008442e..477117eb 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -28,7 +28,7 @@ jobs: uses: docker/build-push-action@v6 with: context: . - file: ./Dockerfile.playwright + file: ./_docker/Dockerfile.playwright push: false push_latest_to_registry: needs: [ test_frontend ] @@ -65,7 +65,7 @@ jobs: VERSION=${{ env.LATEST_TAG }} REVISION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }} platforms: linux/amd64,linux/arm64,linux/arm/v7 - file: ./Dockerfile + file: ./_docker/Dockerfile push: true tags: 'gtstef/filebrowser:latest' labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 47f1e6c1..93a4ff78 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -30,7 +30,7 @@ jobs: uses: docker/build-push-action@v6 with: context: . - file: ./Dockerfile.playwright + file: ./_docker/Dockerfile.playwright push: false push_pr_to_registry: name: Push PR @@ -56,7 +56,7 @@ jobs: uses: docker/build-push-action@v6 with: context: . - file: ./Dockerfile + file: ./_docker/Dockerfile push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/release_beta.yaml b/.github/workflows/release_beta.yaml index e07e6df5..daf5f6ba 100644 --- a/.github/workflows/release_beta.yaml +++ b/.github/workflows/release_beta.yaml @@ -31,7 +31,7 @@ jobs: uses: docker/build-push-action@v6 with: context: . - file: ./Dockerfile.playwright + file: ./_docker/Dockerfile.playwright push: false create_release_tag: needs: [ test_frontend ] @@ -101,7 +101,7 @@ jobs: VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }} REVISION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }} platforms: linux/amd64,linux/arm64,linux/arm/v7 - file: ./Dockerfile + file: ./_docker/Dockerfile push: true tags: ${{ steps.modify-json.outputs.cleaned_tag }} labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/release_dev.yaml b/.github/workflows/release_dev.yaml index a9e36f39..1b3337b3 100644 --- a/.github/workflows/release_dev.yaml +++ b/.github/workflows/release_dev.yaml @@ -44,7 +44,7 @@ jobs: VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }} REVISION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }} platforms: linux/amd64 - file: ./Dockerfile + file: ./_docker/Dockerfile push: true tags: ${{ steps.modify-json.outputs.cleaned_tag }} labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/release_stable.yaml b/.github/workflows/release_stable.yaml index 3bf398af..2792c396 100644 --- a/.github/workflows/release_stable.yaml +++ b/.github/workflows/release_stable.yaml @@ -31,7 +31,7 @@ jobs: uses: docker/build-push-action@v6 with: context: . - file: ./Dockerfile.playwright + file: ./_docker/Dockerfile.playwright push: false create_release_tag: needs: [ test_frontend ] @@ -100,7 +100,7 @@ jobs: VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }} REVISION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }} platforms: linux/amd64,linux/arm64,linux/arm/v7 - file: ./Dockerfile + file: ./_docker/Dockerfile push: true tags: ${{ steps.modify-json.outputs.cleaned_tag }} labels: ${{ steps.meta.outputs.labels }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ae92842..d60ad5ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,27 @@ All notable changes to this project will be documented in this file. For commit guidelines, please refer to [Standard Version](https://github.com/conventional-changelog/standard-version). +## v0.5.0-beta + + > Note: This Beta release includes a configuration change: `auth.method` is now deprecated. This is done to allow multiple login methods at once. Auth methods are specified via `auth.methods` instead. see [example on the wiki](https://github.com/gtsteffaniak/filebrowser/wiki/Configuration#example-auth-config). + + **New Features**: + - Upload progress notification https://github.com/gtsteffaniak/filebrowser/issues/303 + - proxy auth auto create user when `auth.methods.proxy.createUser: true` while using proxy auth. + + **Notes**: + - Context menu positioning tweaks. + - using /tmp cachedir is disabled by default, cache dir can be specified via `server.cacheDir: /tmp` to enable it. https://github.com/gtsteffaniak/filebrowser/issues/326 + + **Bugfixes**: + - Gracefully shutdown to protect database. https://github.com/gtsteffaniak/filebrowser/issues/317 + - validates auth method provided before server startup. + - fix sidebar disk space usage calculation. https://github.com/gtsteffaniak/filebrowser/issues/315 + - Fixed proxy auth header support (make sure your proxy and server are secure!). https://github.com/gtsteffaniak/filebrowser/issues/322 + ## v0.4.2-beta - **New Features** + **New Features**: - Hidden files changes - windows hidden file properties are respected -- when running on windows binary (not docker) with NTFS filesystem. - windows "system" files are considered hidden. @@ -19,10 +37,10 @@ All notable changes to this project will be documented in this file. For commit ## v0.4.1-beta - **New Features** + **New Features**: - right-click actions are available on search. https://github.com/gtsteffaniak/filebrowser/issues/273 - **Notes** + **Notes**: - delete prompt now lists all items that will be affected by delete - Debug and logger output tweaks. @@ -34,13 +52,13 @@ All notable changes to this project will be documented in this file. For commit ## v0.4.0-beta - **New Features** + **New Features**: - Better logging https://github.com/gtsteffaniak/filebrowser/issues/288 - highly configurable - api logs include user - onlyOffice support for editing only office files (inspired from https://github.com/filebrowser/filebrowser/pull/2954) - **Notes** + **Notes**: - Breadcrumbs will only show on file listing (not on previews or editors) - Config file is now optional. It will run with default settings without one and throw a `[WARN ]` message. - Added more descriptions to swagger API @@ -58,7 +76,7 @@ All notable changes to this project will be documented in this file. For commit ## v0.3.6-beta - **New Features** + **New Features**: - Adds "externalUrl" server config https://github.com/gtsteffaniak/filebrowser/issues/272 **Notes**: @@ -74,7 +92,7 @@ All notable changes to this project will be documented in this file. For commit ## v0.3.5 - **New Features** + **New Features**: - More indexing configuration options possible. However consider waiting on using this feature, because I will soon have a full onboarding experience in the UI to manage sources instead. - added config file options "sources" in the server config. - can enable/disable indexing a specified list of directories/files diff --git a/README.md b/README.md index 1f43358b..67031960 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,17 @@ -

- License: Apache-2.0 -

-

- -

-

FileBrowser Quantum - A modern web-based file manager

-

+

+ + [![Go Report Card](https://goreportcard.com/badge/github.com/gtsteffaniak/filebrowser/backend)](https://goreportcard.com/report/github.com/gtsteffaniak/filebrowser/backend) + [![latest version](https://img.shields.io/github/release/gtsteffaniak/filebrowser/all.svg)](https://github.com/gtsteffaniak/filebrowser/releases) + [![Apache-2.0 License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0) + [![Codacy Badge](https://app.codacy.com/project/badge/Grade/1c48cfb7646d4009aa8c6f71287670b8)](https://www.codacy.com/gh/gtsteffaniak/filebrowser/dashboard) + [![DockerHub Pulls](https://img.shields.io/docker/pulls/gtstef/filebrowser?label=latest%20Docker%20pulls)](https://hub.docker.com/r/gtstef/filebrowser) + + +

FileBrowser Quantum

+ A modern web-based file manager +

-

+
> [!WARNING] > There is no stable version -- planned 2025. diff --git a/Dockerfile b/_docker/Dockerfile similarity index 100% rename from Dockerfile rename to _docker/Dockerfile diff --git a/Dockerfile.playwright b/_docker/Dockerfile.playwright similarity index 100% rename from Dockerfile.playwright rename to _docker/Dockerfile.playwright diff --git a/Dockerfile.playwright-base b/_docker/Dockerfile.playwright-base similarity index 100% rename from Dockerfile.playwright-base rename to _docker/Dockerfile.playwright-base diff --git a/_docker/docker-compose.yaml b/_docker/docker-compose.yaml new file mode 100644 index 00000000..4bc9afc0 --- /dev/null +++ b/_docker/docker-compose.yaml @@ -0,0 +1,15 @@ +services: + nginx-proxy-auth: + image: nginx + container_name: nginx-proxy-auth + ports: + - "8080:80" + volumes: + - ./src/default.conf:/etc/nginx/conf.d/default.conf + filebrowser: + volumes: + - '../frontend:/home/frontend' + - "./src/config.yaml:/home/filebrowser/config.yaml" + build: + context: ../ + dockerfile: ./_docker/Dockerfile diff --git a/_docker/src/config.yaml b/_docker/src/config.yaml new file mode 100644 index 00000000..8ffa1fdc --- /dev/null +++ b/_docker/src/config.yaml @@ -0,0 +1,8 @@ +server: + port: 80 + baseURL: "/" + root: "../frontend/tests/playwright-files" +auth: + method: proxy + header: X-Username + signup: false \ No newline at end of file diff --git a/_docker/src/default.conf b/_docker/src/default.conf new file mode 100644 index 00000000..e301ce74 --- /dev/null +++ b/_docker/src/default.conf @@ -0,0 +1,13 @@ +server { + listen 80; + server_name localhost 127.0.0.1; + + location / { + proxy_pass http://filebrowser; + proxy_set_header X-Username "proxy-user"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} \ No newline at end of file diff --git a/backend/auth/storage.go b/backend/auth/storage.go index ee51dc2c..b11c5bf0 100644 --- a/backend/auth/storage.go +++ b/backend/auth/storage.go @@ -17,8 +17,25 @@ type Storage struct { } // NewStorage creates a auth storage from a backend. -func NewStorage(back StorageBackend, userStore *users.Storage) *Storage { - return &Storage{back: back, users: userStore} +func NewStorage(back StorageBackend, userStore *users.Storage) (*Storage, error) { + store := &Storage{back: back, users: userStore} + err := store.Save(&JSONAuth{}) + if err != nil { + return nil, err + } + err = store.Save(&ProxyAuth{}) + if err != nil { + return nil, err + } + err = store.Save(&HookAuth{}) + if err != nil { + return nil, err + } + err = store.Save(&NoAuth{}) + if err != nil { + return nil, err + } + return store, nil } // Get wraps a StorageBackend.Get. diff --git a/backend/cmd/root.go b/backend/cmd/root.go index 94ba3c6f..40152ede 100644 --- a/backend/cmd/root.go +++ b/backend/cmd/root.go @@ -1,10 +1,13 @@ package cmd import ( + "context" "flag" "fmt" "os" + "os/signal" "strings" + "syscall" "github.com/gtsteffaniak/filebrowser/backend/diskcache" "github.com/gtsteffaniak/filebrowser/backend/files" @@ -71,6 +74,15 @@ func StartFilebrowser() { setCmd.StringVar(&scope, "s", "", "Specify a user scope, otherwise default user config scope is used") setCmd.StringVar(&dbConfig, "c", "config.yaml", "Path to the config file, default: config.yaml") + // Create context and channels for graceful shutdown + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + done := make(chan struct{}) // Signals server has stopped + shutdownComplete := make(chan struct{}) // Signals shutdown process is complete + signalChan := make(chan os.Signal, 1) + signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM) + // Parse subcommand flags only if a subcommand is specified if len(os.Args) > 1 { switch os.Args[1] { @@ -116,6 +128,7 @@ Release Info : https://github.com/gtsteffaniak/filebrowser/releases/tag/%v return } } + store, dbExists := getStore(configPath) database := fmt.Sprintf("Using existing database : %v", settings.Config.Server.Database) if !dbExists { @@ -125,9 +138,22 @@ Release Info : https://github.com/gtsteffaniak/filebrowser/releases/tag/%v for _, v := range settings.Config.Server.Sources { sources = append(sources, v.Name+": "+v.Path) } + + authMethods := []string{} + if settings.Config.Auth.Methods.PasswordAuth { + authMethods = append(authMethods, "Password") + } + if settings.Config.Auth.Methods.ProxyAuth.Enabled { + authMethods = append(authMethods, "Proxy") + } + if settings.Config.Auth.Methods.NoAuth { + logger.Warning("Configured with no authentication, this is not recommended.") + authMethods = []string{"Disabled"} + } + logger.Info(fmt.Sprintf("Initializing FileBrowser Quantum (%v)", version.Version)) logger.Info(fmt.Sprintf("Using Config file : %v", configPath)) - logger.Debug(fmt.Sprintf("Embeded frontend : %v", os.Getenv("FILEBROWSER_NO_EMBEDED") != "true")) + logger.Info(fmt.Sprintf("Auth Methods : %v", authMethods)) logger.Info(database) logger.Info(fmt.Sprintf("Sources : %v", sources)) serverConfig := settings.Config.Server @@ -142,18 +168,34 @@ Release Info : https://github.com/gtsteffaniak/filebrowser/releases/tag/%v for _, source := range sourceConfigs { go files.Initialize(source) } - if err := rootCMD(store, &serverConfig); err != nil { - logger.Fatal(fmt.Sprintf("Error starting filebrowser: %v", err)) + // Start the rootCMD in a goroutine + go func() { + if err := rootCMD(ctx, store, &serverConfig, shutdownComplete); err != nil { + logger.Fatal(fmt.Sprintf("Error starting filebrowser: %v", err)) + } + close(done) // Signal that the server has stopped + }() + // Wait for a shutdown signal or the server to stop + select { + case <-signalChan: + logger.Info("Received shutdown signal. Shutting down gracefully...") + cancel() // Trigger context cancellation + case <-done: + logger.Info("Server stopped unexpectedly. Shutting down...") } + + <-shutdownComplete // Ensure we don't exit prematurely + // Wait for the server to stop + logger.Info("Shutdown complete.") } -func rootCMD(store *storage.Storage, serverConfig *settings.Server) error { +func rootCMD(ctx context.Context, store *storage.Storage, serverConfig *settings.Server, shutdownComplete chan struct{}) error { if serverConfig.NumImageProcessors < 1 { logger.Fatal("Image resize workers count could not be < 1") } imgSvc := img.New(serverConfig.NumImageProcessors) - cacheDir := "/tmp" + cacheDir := settings.Config.Server.CacheDir var fileCache diskcache.Interface // Use file cache if cacheDir is specified @@ -167,7 +209,7 @@ func rootCMD(store *storage.Storage, serverConfig *settings.Server) error { // No-op cache if no cacheDir is specified fileCache = diskcache.NewNoOp() } - fbhttp.StartHttp(imgSvc, store, fileCache) + fbhttp.StartHttp(ctx, imgSvc, store, fileCache, shutdownComplete) return nil } diff --git a/backend/config.yaml b/backend/config.yaml index d67af686..22799084 100644 --- a/backend/config.yaml +++ b/backend/config.yaml @@ -3,7 +3,6 @@ server: baseURL: "/" root: "/srv" auth: - method: password signup: false userDefaults: darkMode: true diff --git a/backend/http/auth.go b/backend/http/auth.go index a667a755..79ce2977 100644 --- a/backend/http/auth.go +++ b/backend/http/auth.go @@ -75,8 +75,13 @@ func extractToken(r *http.Request) (string, error) { } func loginHandler(w http.ResponseWriter, r *http.Request) { + if !config.Auth.Methods.PasswordAuth { + http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden) + return + } + // currently only supports user/pass // Get the authentication method from the settings - auther, err := store.Auth.Get(config.Auth.Method) + auther, err := store.Auth.Get("password") if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return diff --git a/backend/http/middleware.go b/backend/http/middleware.go index 673f287a..e01afefa 100644 --- a/backend/http/middleware.go +++ b/backend/http/middleware.go @@ -13,6 +13,7 @@ import ( "github.com/gtsteffaniak/filebrowser/backend/files" "github.com/gtsteffaniak/filebrowser/backend/logger" "github.com/gtsteffaniak/filebrowser/backend/runner" + "github.com/gtsteffaniak/filebrowser/backend/settings" "github.com/gtsteffaniak/filebrowser/backend/users" ) @@ -93,15 +94,44 @@ func withAdminHelper(fn handleFunc) handleFunc { // Middleware to retrieve and authenticate user func withUserHelper(fn handleFunc) handleFunc { return func(w http.ResponseWriter, r *http.Request, data *requestContext) (int, error) { - if config.Auth.Method == "noauth" { + if config.Auth.Methods.NoAuth { var err error // Retrieve the user from the store and store it in the context - data.user, err = store.Users.Get(files.RootPaths["default"], "admin") + data.user, err = store.Users.Get(files.RootPaths["default"], 1) if err != nil { + logger.Error(fmt.Sprintf("no auth: %v", err)) return http.StatusInternalServerError, err } return fn(w, r, data) } + proxyUser := r.Header.Get(config.Auth.Methods.ProxyAuth.Header) + if config.Auth.Methods.ProxyAuth.Enabled && proxyUser != "" { + var err error + // Retrieve the user from the store and store it in the context + data.user, err = store.Users.Get(files.RootPaths["default"], proxyUser) + if err != nil { + if err.Error() != "the resource does not exist" { + return http.StatusInternalServerError, err + } + if config.Auth.Methods.ProxyAuth.CreateUser { + newUser := settings.ApplyUserDefaults(users.User{ + Username: proxyUser, + }) + err := store.Users.Save(&newUser) + if err != nil { + return http.StatusInternalServerError, err + } + data.user, err = store.Users.Get(files.RootPaths["default"], proxyUser) + if err != nil { + return http.StatusInternalServerError, err + } + } else { + return http.StatusUnauthorized, fmt.Errorf("proxy authentication failed - no user found") + } + } + setUserInResponseWriter(w, data.user) + return fn(w, r, data) + } keyFunc := func(token *jwt.Token) (interface{}, error) { return config.Auth.Key, nil } diff --git a/backend/http/router.go b/backend/http/router.go index 88686d22..2004507e 100644 --- a/backend/http/router.go +++ b/backend/http/router.go @@ -1,6 +1,7 @@ package http import ( + "context" "crypto/tls" "embed" "fmt" @@ -8,6 +9,7 @@ import ( "net/http" "os" "text/template" + "time" "github.com/gtsteffaniak/filebrowser/backend/logger" "github.com/gtsteffaniak/filebrowser/backend/settings" @@ -43,7 +45,7 @@ var ( assetFs fs.FS ) -func StartHttp(Service ImgService, storage *storage.Storage, cache FileCache) { +func StartHttp(ctx context.Context, Service ImgService, storage *storage.Storage, cache FileCache, shutdownComplete chan struct{}) { store = storage fileCache = cache @@ -140,51 +142,74 @@ func StartHttp(Service ImgService, storage *storage.Storage, cache FileCache) { var scheme string port := "" - - // Determine whether to use HTTPS (TLS) or HTTP - if config.Server.TLSCert != "" && config.Server.TLSKey != "" { - // Load the TLS certificate and key - cer, err := tls.LoadX509KeyPair(config.Server.TLSCert, config.Server.TLSKey) - if err != nil { - logger.Fatal(fmt.Sprintf("could not load certificate: %v", err)) - } - - // Create a custom TLS listener - tlsConfig := &tls.Config{ - MinVersion: tls.VersionTLS12, - Certificates: []tls.Certificate{cer}, - } - - // Set HTTPS scheme and default port for TLS - scheme = "https" - - // Listen on TCP and wrap with TLS - listener, err := tls.Listen("tcp", fmt.Sprintf(":%v", config.Server.Port), tlsConfig) - if err != nil { - logger.Fatal(fmt.Sprintf("could not start TLS server: %v", err)) - } - if config.Server.Port != 443 { - port = fmt.Sprintf(":%d", config.Server.Port) - } - // Build the full URL with host and port - fullURL := fmt.Sprintf("%s://localhost%s%s", scheme, port, config.Server.BaseURL) - logger.Info(fmt.Sprintf("Running at : %s", fullURL)) - err = http.Serve(listener, muxWithMiddleware(router)) - if err != nil { - logger.Fatal(fmt.Sprintf("could not start server: %v", err)) - } - } else { - // Set HTTP scheme and the default port for HTTP - scheme = "http" - if config.Server.Port != 80 { - port = fmt.Sprintf(":%d", config.Server.Port) - } - // Build the full URL with host and port - fullURL := fmt.Sprintf("%s://localhost%s%s", scheme, port, config.Server.BaseURL) - logger.Info(fmt.Sprintf("Running at : %s", fullURL)) - err := http.ListenAndServe(fmt.Sprintf(":%v", config.Server.Port), muxWithMiddleware(router)) - if err != nil { - logger.Fatal(fmt.Sprintf("could not start server: %v", err)) - } + srv := &http.Server{ + Addr: fmt.Sprintf(":%v", config.Server.Port), + Handler: muxWithMiddleware(router), } + go func() { + // Determine whether to use HTTPS (TLS) or HTTP + if config.Server.TLSCert != "" && config.Server.TLSKey != "" { + // Load the TLS certificate and key + cer, err := tls.LoadX509KeyPair(config.Server.TLSCert, config.Server.TLSKey) + if err != nil { + logger.Fatal(fmt.Sprintf("Could not load certificate: %v", err)) + } + + // Create a custom TLS configuration + tlsConfig := &tls.Config{ + MinVersion: tls.VersionTLS12, + Certificates: []tls.Certificate{cer}, + } + + // Set HTTPS scheme and default port for TLS + scheme = "https" + if config.Server.Port != 443 { + port = fmt.Sprintf(":%d", config.Server.Port) + } + + // Build the full URL with host and port + fullURL := fmt.Sprintf("%s://localhost%s%s", scheme, port, config.Server.BaseURL) + logger.Info(fmt.Sprintf("Running at : %s", fullURL)) + + // Create a TLS listener and serve + listener, err := tls.Listen("tcp", srv.Addr, tlsConfig) + if err != nil { + logger.Fatal(fmt.Sprintf("Could not start TLS server: %v", err)) + } + if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed { + logger.Fatal(fmt.Sprintf("Server error: %v", err)) + } + } else { + // Set HTTP scheme and the default port for HTTP + scheme = "http" + if config.Server.Port != 80 { + port = fmt.Sprintf(":%d", config.Server.Port) + } + + // Build the full URL with host and port + fullURL := fmt.Sprintf("%s://localhost%s%s", scheme, port, config.Server.BaseURL) + logger.Info(fmt.Sprintf("Running at : %s", fullURL)) + + // Start HTTP server + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + logger.Fatal(fmt.Sprintf("Server error: %v", err)) + } + } + }() + + // Wait for context cancellation to shut down the server + <-ctx.Done() + logger.Info("Shutting down HTTP server...") + + // Graceful shutdown with a timeout - 30 seconds, in case downloads are happening + shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + if err := srv.Shutdown(shutdownCtx); err != nil { + logger.Error(fmt.Sprintf("HTTP server forced to shut down: %v", err)) + } else { + logger.Info("HTTP server shut down gracefully.") + } + + // Signal that shutdown is complete + close(shutdownComplete) } diff --git a/backend/http/static.go b/backend/http/static.go index e5f10301..4a04eb31 100644 --- a/backend/http/static.go +++ b/backend/http/static.go @@ -36,7 +36,7 @@ func (t *TemplateRenderer) Render(w http.ResponseWriter, name string, data inter func handleWithStaticData(w http.ResponseWriter, r *http.Request, file, contentType string) { w.Header().Set("Content-Type", contentType) - auther, err := store.Auth.Get(config.Auth.Method) + auther, err := store.Auth.Get("password") if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return @@ -53,8 +53,8 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, file, contentT "CommitSHA": version.CommitSHA, "StaticURL": config.Server.BaseURL + "static", "Signup": settings.Config.Auth.Signup, - "NoAuth": config.Auth.Method == "noauth", - "AuthMethod": config.Auth.Method, + "NoAuth": config.Auth.Methods.NoAuth, + "PasswordAuth": config.Auth.Methods.PasswordAuth, "LoginPage": auther.LoginPage(), "CSS": false, "ReCaptcha": false, @@ -80,8 +80,8 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, file, contentT } } - if config.Auth.Method == "password" { - raw, err := store.Auth.Get(config.Auth.Method) //nolint:govet + if config.Auth.Methods.PasswordAuth { + raw, err := store.Auth.Get("password") //nolint:govet if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return diff --git a/backend/settings/config.go b/backend/settings/config.go index a234cce9..20e122ab 100644 --- a/backend/settings/config.go +++ b/backend/settings/config.go @@ -46,7 +46,9 @@ func Initialize(configFile string) { log.Println("[ERROR] Failed to set up logger:", err) } } - + if Config.Auth.Method != "" { + logger.Warning("The `auth.method` setting is deprecated and will be removed in a future version. Please use `auth.methods` instead.") + } Config.UserDefaults.Perm = Config.UserDefaults.Permissions // Convert relative path to absolute path if len(Config.Server.Sources) > 0 { @@ -129,14 +131,22 @@ func setDefaults() Settings { Root: ".", }, Auth: Auth{ - TokenExpirationHours: 2, AdminUsername: "admin", AdminPassword: "admin", - Method: "password", + TokenExpirationHours: 2, Signup: false, Recaptcha: Recaptcha{ Host: "", }, + Methods: LoginMethods{ + ProxyAuth: ProxyAuthConfig{ + Enabled: false, + CreateUser: false, + Header: "", + }, + NoAuth: false, + PasswordAuth: true, + }, }, Frontend: Frontend{ Name: "FileBrowser Quantum", @@ -145,8 +155,8 @@ func setDefaults() Settings { StickySidebar: true, Scope: ".", LockPassword: false, - ShowHidden: true, - DarkMode: false, + ShowHidden: false, + DarkMode: true, DisableSettings: false, ViewMode: "normal", Locale: "en", diff --git a/backend/settings/settings_test.go b/backend/settings/settings_test.go index 8a79150e..54d62c29 100644 --- a/backend/settings/settings_test.go +++ b/backend/settings/settings_test.go @@ -40,8 +40,6 @@ func TestConfigLoadSpecificValues(t *testing.T) { globalVal interface{} newVal interface{} }{ - {"Auth.Method", Config.Auth.Method, newConfig.Auth.Method}, - {"Auth.Method", Config.Auth.Method, newConfig.Auth.Method}, {"Server.Database", Config.Server.Database, newConfig.Server.Database}, } diff --git a/backend/settings/structs.go b/backend/settings/structs.go index 2c29aa93..ad9718b8 100644 --- a/backend/settings/structs.go +++ b/backend/settings/structs.go @@ -4,6 +4,14 @@ import ( "github.com/gtsteffaniak/filebrowser/backend/users" ) +type AllowedMethods string + +const ( + ProxyAuth AllowedMethods = "proxyAuth" + NoAuth AllowedMethods = "noAuth" + PasswordAuth AllowedMethods = "passwordAuth" +) + type Settings struct { Commands map[string][]string `json:"commands"` Shell []string `json:"shell"` @@ -17,16 +25,28 @@ type Settings struct { } type Auth struct { - TokenExpirationHours int `json:"tokenExpirationHours"` - Recaptcha Recaptcha `json:"recaptcha"` - Header string `json:"header"` - Method string `json:"method"` - Command string `json:"command"` - Signup bool `json:"signup"` - Shell string `json:"shell"` - AdminUsername string `json:"adminUsername"` - AdminPassword string `json:"adminPassword"` - Key []byte `json:"key"` + TokenExpirationHours int `json:"tokenExpirationHours"` + Recaptcha Recaptcha `json:"recaptcha"` + Methods LoginMethods `json:"methods"` + Command string `json:"command"` + Signup bool `json:"signup"` + Method string `json:"method"` + Shell string `json:"shell"` + Key []byte `json:"key"` + AdminUsername string `json:"adminUsername"` + AdminPassword string `json:"adminPassword"` +} + +type LoginMethods struct { + ProxyAuth ProxyAuthConfig `json:"proxy"` + NoAuth bool `json:"noauth"` + PasswordAuth bool `json:"password"` +} + +type ProxyAuthConfig struct { + Enabled bool `json:"enabled"` + CreateUser bool `json:"createUser"` + Header string `json:"header"` } type Recaptcha struct { @@ -54,6 +74,7 @@ type Server struct { Sources []Source `json:"sources"` ExternalUrl string `json:"externalUrl"` InternalUrl string `json:"internalUrl"` // used by integrations + CacheDir string `json:"cacheDir"` } type Integrations struct { diff --git a/backend/settings/testingConfig.yaml b/backend/settings/testingConfig.yaml index acbe08a6..1c31bef3 100644 --- a/backend/settings/testingConfig.yaml +++ b/backend/settings/testingConfig.yaml @@ -16,7 +16,6 @@ auth: key: "" secret: "" header: "" - method: json command: "" signup: false shell: "" diff --git a/backend/storage/bolt/bolt.go b/backend/storage/bolt/bolt.go index 55148e9d..7d8fecdc 100644 --- a/backend/storage/bolt/bolt.go +++ b/backend/storage/bolt/bolt.go @@ -14,6 +14,9 @@ func NewStorage(db *storm.DB) (*auth.Storage, *users.Storage, *share.Storage, *s userStore := users.NewStorage(usersBackend{db: db}) shareStore := share.NewStorage(shareBackend{db: db}) settingsStore := settings.NewStorage(settingsBackend{db: db}) - authStore := auth.NewStorage(authBackend{db: db}, userStore) + authStore, err := auth.NewStorage(authBackend{db: db}, userStore) + if err != nil { + return nil, nil, nil, nil, err + } return authStore, userStore, shareStore, settingsStore, nil } diff --git a/backend/storage/bolt/users.go b/backend/storage/bolt/users.go index 02d77f1d..a7281917 100644 --- a/backend/storage/bolt/users.go +++ b/backend/storage/bolt/users.go @@ -19,9 +19,11 @@ func (st usersBackend) GetBy(i interface{}) (user *users.User, err error) { user = &users.User{} var arg string - switch i.(type) { + switch val := i.(type) { case uint: arg = "ID" + case int: + i = uint(val) case string: arg = "Username" default: diff --git a/backend/storage/storage.go b/backend/storage/storage.go index 17f53834..43c66a8a 100644 --- a/backend/storage/storage.go +++ b/backend/storage/storage.go @@ -80,14 +80,6 @@ func dbExists(path string) (bool, error) { func quickSetup(store *Storage) { settings.Config.Auth.Key = utils.GenerateKey() - if settings.Config.Auth.Method == "noauth" { - err := store.Auth.Save(&auth.NoAuth{}) - utils.CheckErr("store.Auth.Save", err) - } else { - settings.Config.Auth.Method = "password" - err := store.Auth.Save(&auth.JSONAuth{}) - utils.CheckErr("store.Auth.Save", err) - } err := store.Settings.Save(&settings.Config) utils.CheckErr("store.Settings.Save", err) err = store.Settings.SaveServer(&settings.Config.Server) diff --git a/backend/swagger/docs/docs.go b/backend/swagger/docs/docs.go index 156dcbc0..f4148356 100644 --- a/backend/swagger/docs/docs.go +++ b/backend/swagger/docs/docs.go @@ -1184,6 +1184,10 @@ const docTemplate = `{ "$ref": "#/definitions/files.ItemInfo" } }, + "hidden": { + "description": "whether the file is hidden", + "type": "boolean" + }, "modified": { "description": "modification time", "type": "string" @@ -1209,6 +1213,10 @@ const docTemplate = `{ "files.ItemInfo": { "type": "object", "properties": { + "hidden": { + "description": "whether the file is hidden", + "type": "boolean" + }, "modified": { "description": "modification time", "type": "string" @@ -1362,9 +1370,6 @@ const docTemplate = `{ "gallerySize": { "type": "integer" }, - "hideDotfiles": { - "type": "boolean" - }, "locale": { "type": "string" }, @@ -1386,6 +1391,9 @@ const docTemplate = `{ "scope": { "type": "string" }, + "showHidden": { + "type": "boolean" + }, "singleClick": { "type": "boolean" }, @@ -1570,9 +1578,6 @@ const docTemplate = `{ "gallerySize": { "type": "integer" }, - "hideDotfiles": { - "type": "boolean" - }, "id": { "type": "integer" }, @@ -1597,6 +1602,9 @@ const docTemplate = `{ "scope": { "type": "string" }, + "showHidden": { + "type": "boolean" + }, "singleClick": { "type": "boolean" }, diff --git a/backend/swagger/docs/swagger.json b/backend/swagger/docs/swagger.json index b74fd341..fd91588d 100644 --- a/backend/swagger/docs/swagger.json +++ b/backend/swagger/docs/swagger.json @@ -1173,6 +1173,10 @@ "$ref": "#/definitions/files.ItemInfo" } }, + "hidden": { + "description": "whether the file is hidden", + "type": "boolean" + }, "modified": { "description": "modification time", "type": "string" @@ -1198,6 +1202,10 @@ "files.ItemInfo": { "type": "object", "properties": { + "hidden": { + "description": "whether the file is hidden", + "type": "boolean" + }, "modified": { "description": "modification time", "type": "string" @@ -1351,9 +1359,6 @@ "gallerySize": { "type": "integer" }, - "hideDotfiles": { - "type": "boolean" - }, "locale": { "type": "string" }, @@ -1375,6 +1380,9 @@ "scope": { "type": "string" }, + "showHidden": { + "type": "boolean" + }, "singleClick": { "type": "boolean" }, @@ -1559,9 +1567,6 @@ "gallerySize": { "type": "integer" }, - "hideDotfiles": { - "type": "boolean" - }, "id": { "type": "integer" }, @@ -1586,6 +1591,9 @@ "scope": { "type": "string" }, + "showHidden": { + "type": "boolean" + }, "singleClick": { "type": "boolean" }, diff --git a/backend/swagger/docs/swagger.yaml b/backend/swagger/docs/swagger.yaml index eaeac072..582b43c9 100644 --- a/backend/swagger/docs/swagger.yaml +++ b/backend/swagger/docs/swagger.yaml @@ -11,6 +11,9 @@ definitions: items: $ref: '#/definitions/files.ItemInfo' type: array + hidden: + description: whether the file is hidden + type: boolean modified: description: modification time type: string @@ -29,6 +32,9 @@ definitions: type: object files.ItemInfo: properties: + hidden: + description: whether the file is hidden + type: boolean modified: description: modification time type: string @@ -130,8 +136,6 @@ definitions: type: boolean gallerySize: type: integer - hideDotfiles: - type: boolean locale: type: string lockPassword: @@ -146,6 +150,8 @@ definitions: type: array scope: type: string + showHidden: + type: boolean singleClick: type: boolean sorting: @@ -269,8 +275,6 @@ definitions: type: boolean gallerySize: type: integer - hideDotfiles: - type: boolean id: type: integer locale: @@ -287,6 +291,8 @@ definitions: type: array scope: type: string + showHidden: + type: boolean singleClick: type: boolean sorting: diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts index 3d45163e..61627509 100644 --- a/frontend/playwright.config.ts +++ b/frontend/playwright.config.ts @@ -16,7 +16,7 @@ export default defineConfig({ /* Run tests in files in parallel */ fullyParallel: false, /* Fail the build on CI if you accidentally left test.only in the source code. */ - forbidOnly: Boolean(process.env.CI), + forbidOnly: false, /* Retry on CI only */ retries: 2, /* Opt out of parallel tests on CI. */ diff --git a/frontend/public/index.html b/frontend/public/index.html index 83b9e8fa..7584acc9 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -28,6 +28,11 @@ + + + + + + + diff --git a/frontend/src/components/settings/UserForm.vue b/frontend/src/components/settings/UserForm.vue index 1ad843c5..281c49a5 100644 --- a/frontend/src/components/settings/UserForm.vue +++ b/frontend/src/components/settings/UserForm.vue @@ -1,5 +1,5 @@