Repository: jkroepke/openvpn-auth-oauth2 (go)

Issue:
Asset Path not working
### Current Behavior

When I set asset-path in config.yaml, no custom assets are used.

### Expected Behavior

When I set asset-path, my customized assets should be used.

### Steps To Reproduce

```Markdown
1. On Debian 12 server
2. With the config : 

#debug:
#  pprof: false
#  listen: :9001
http:
  assets-path: "/etc/openvpn-auth-oauth2/assets/" # Example: "/etc/openvpn-auth-oauth2/assets/"
  baseurl: "https://vpn.mydomain.tld/"
#  cert: ""
#  check:
#    ipaddr: false
#  enable-proxy-headers: true
#  key: ""
#  listen: ":9000"
  secret: "mysupersecret"
#  template: "" # Path to a HTML file which is displayed at the end of the screen
#  tls: false

3. My assets are sued
```

### Environment

- openvpn-auth-oauth2 Version: 1.22.7
- OpenVPN Server Version: 2.6.3
- Server OS: Linux Debian 12
- OpenVPN Client (flavor, OS): Windows with OpenVPN Community 2.6.13
- OIDC Provider: Authentik


### openvpn-auth-oauth2 logs

```shell
journalctl -flu openvpn-auth-oauth2

systemd[1]: Started openvpn-auth-oauth2.service - OpenVPN authenticator.
openvpn-auth-oauth2[763]: time=2025-03-20T17:58:16.730+01:00 level=INFO msg="manually configure oauth2 provider with provider generic and providerConfig https://sso.mydomain.tld/application/o/authorize/ and https://sso.mydomain.tld/application/o/token/"
openvpn-auth-oauth2[763]: time=2025-03-20T17:58:16.730+01:00 level=WARN msg="generic provider with manual configuration is used. Validation of user data is not possible."
openvpn-auth-oauth2[763]: time=2025-03-20T17:58:16.732+01:00 level=INFO msg="start HTTP server listener on [::]:9000"
openvpn-auth-oauth2[763]: time=2025-03-20T17:58:16.732+01:00 level=INFO msg="connect to openvpn management interface unix:///run/openvpn/server.sock"
openvpn-auth-oauth2[763]: time=2025-03-20T17:58:16.733+01:00 level=INFO msg="openvpn-auth-oauth2 started with base url https://vpn.mydomain.tld/"
openvpn-auth-oauth2[763]: time=2025-03-20T17:58:16.733+01:00 level=INFO msg="connection to OpenVPN management interface established"
openvpn-auth-oauth2[763]: time=2025-03-20T17:58:16.741+01:00 level=INFO msg="OpenVPN Version: OpenVPN 2.6.3 x86_64-pc-linux-gnu [SSL (OpenSSL)] [LZO] [LZ4] [EPOLL] [PKCS11] [MH/PKTINFO] [AEAD] [DCO] - Management Version: 5"


dmesg | grep openvpn

[    4.815419] systemd[1]: Created slice system-openvpn.slice - Slice /system/openvpn.
```

### openvpn server logs

```shell

```

### Anything else?

_No response_

### Preflight Checklist

- [x] I could not find a solution in the [documentation](https://github.com/jkroepke/openvpn-auth-oauth2/wiki),
the [FAQ](https://github.com/jkroepke/openvpn-auth-oauth2/wiki/FAQ), the existing issues or discussions.


Pull request description:
make assets directory configurable again
#### What this PR does / why we need it

#### Which issue this PR fixes

*(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close that issue when PR gets merged)*: fixes #

- fixes #444 

#### Special notes for your reviewer

#### Particularly user-facing changes

#### Checklist

Complete these before marking the PR as `ready to review`:

<!-- [Place an '[x]' (no spaces) in all applicable fields.] -->

- [ ] [DCO](https://github.com/prometheus-community/helm-charts/blob/main/CONTRIBUTING.md#sign-off-your-work) signed
- [ ] The PR title has a summary of the changes
- [ ] The PR body has a summary to reflect any significant (and particularly user-facing) changes introduced by this PR


Patch:
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index ab33d58..1db2fa5 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -34,7 +34,7 @@ jobs:
           GPG_KEY: ${{ secrets.GPG_KEY }}
 
       - name: go build (with goreleaser)
-        uses: goreleaser/goreleaser-action@90a3faa9d0182683851fbfa97ca1a2cb983bfca3 # v6.2.1
+        uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 # v6.3.0
         with:
           # renovate: github=goreleaser/goreleaser
           version: v2.8.1
@@ -139,7 +139,7 @@ jobs:
           GPG_KEY_PATH: "${{ secrets.GPG_KEY_PATH }}"
 
       - name: Run GoReleaser
-        uses: goreleaser/goreleaser-action@90a3faa9d0182683851fbfa97ca1a2cb983bfca3 # v6.2.1
+        uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 # v6.3.0
         with:
           # renovate: github=goreleaser/goreleaser
           version: v2.8.1
diff --git a/cmd/daemon/root.go b/cmd/daemon/root.go
index e4af967..c1a93c4 100644
--- a/cmd/daemon/root.go
+++ b/cmd/daemon/root.go
@@ -6,6 +6,7 @@ import (
 	"flag"
 	"fmt"
 	"io"
+	"io/fs"
 	"log/slog"
 	"net/http"
 	"net/http/pprof"
@@ -24,6 +25,7 @@ import (
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/oauth2/providers/google"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/openvpn"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/tokenstorage"
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/ui"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/utils"
 )
 
@@ -87,13 +89,23 @@ func Execute(args []string, logWriter io.Writer, version, commit, date string) i
 
 	openvpnClient.SetOAuth2Client(oAuth2Client)
 
-	httpHandler, err := httphandler.New(conf, oAuth2Client)
-	if err != nil {
-		logger.Error(err.Error())
+	var assetsFs fs.FS
 
-		return 1
+	if conf.HTTP.AssetPath == "" {
+		assetsFs, err = fs.Sub(ui.Static, "assets")
+		if err != nil {
+			logger.Error("failed to create assets file system",
+				slog.Any("err", err),
+			)
+
+			return 1
+		}
+	} else {
+		assetsFs = os.DirFS(conf.HTTP.AssetPath)
 	}
 
+	httpHandler := httphandler.New(conf, oAuth2Client, assetsFs)
+
 	wg := sync.WaitGroup{}
 	defer wg.Wait()
 
diff --git a/internal/httphandler/handler.go b/internal/httphandler/handler.go
index 7bc54fe..3f644fd 100644
--- a/internal/httphandler/handler.go
+++ b/internal/httphandler/handler.go
@@ -8,15 +8,9 @@ import (
 
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/config"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/oauth2"
-	"github.com/jkroepke/openvpn-auth-oauth2/internal/ui"
 )
 
-func New(conf config.Config, oAuth2Client *oauth2.Client) (*http.ServeMux, error) {
-	staticFs, err := fs.Sub(ui.Static, "assets")
-	if err != nil {
-		return nil, fmt.Errorf("failed to create static file system: %w", err)
-	}
-
+func New(conf config.Config, oAuth2Client *oauth2.Client, assetsFs fs.FS) *http.ServeMux {
 	basePath := strings.TrimSuffix(conf.HTTP.BaseURL.Path, "/")
 
 	mux := http.NewServeMux()
@@ -25,11 +19,11 @@ func New(conf config.Config, oAuth2Client *oauth2.Client) (*http.ServeMux, error
 		w.WriteHeader(http.StatusOK)
 		_, _ = w.Write([]byte("OK"))
 	}))
-	mux.Handle(fmt.Sprintf("GET %s/assets/", basePath), http.StripPrefix(basePath+"/assets/", http.FileServerFS(staticFs)))
+	mux.Handle(fmt.Sprintf("GET %s/assets/", basePath), http.StripPrefix(basePath+"/assets/", http.FileServerFS(assetsFs)))
 	mux.Handle(fmt.Sprintf("GET %s/oauth2/start", basePath), noCacheHeaders(oAuth2Client.OAuth2Start()))
 	mux.Handle(fmt.Sprintf("GET %s/oauth2/callback", basePath), noCacheHeaders(oAuth2Client.OAuth2Callback()))
 
-	return mux, nil
+	return mux
 }
 
 func noCacheHeaders(h http.Handler) http.Handler {


Test patch:
diff --git a/internal/httphandler/handler_test.go b/internal/httphandler/handler_test.go
new file mode 100644
index 0000000..b27cc6a
--- /dev/null
+++ b/internal/httphandler/handler_test.go
@@ -0,0 +1,42 @@
+package httphandler_test
+
+import (
+	"net/http"
+	"testing"
+	"testing/fstest"
+
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/config"
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/httphandler"
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/oauth2"
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/oauth2/providers/generic"
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/utils/testutils"
+	"github.com/stretchr/testify/require"
+)
+
+func TestAssets(t *testing.T) {
+	t.Parallel()
+
+	logger := testutils.NewTestLogger()
+
+	conf := config.Defaults
+	conf.OAuth2.Issuer = &config.URL{Scheme: "http", Host: "localhost"}
+	conf.OAuth2.Endpoints.Discovery = &config.URL{Scheme: "http", Host: "localhost"}
+	conf.OAuth2.Endpoints.Auth = &config.URL{Scheme: "http", Host: "localhost"}
+	conf.OAuth2.Endpoints.Token = &config.URL{Scheme: "http", Host: "localhost"}
+
+	provider, err := generic.NewProvider(t.Context(), conf, http.DefaultClient)
+	require.NoError(t, err)
+
+	oAuth2Client, err := oauth2.New(t.Context(), logger.Logger, conf, http.DefaultClient, testutils.NewFakeStorage(), provider, testutils.NewFakeOpenVPNClient())
+	require.NoError(t, err)
+
+	assetsFs := fstest.MapFS{
+		"index.txt": &fstest.MapFile{
+			Data: []byte("index"),
+		},
+	}
+
+	handler := httphandler.New(config.Defaults, oAuth2Client, assetsFs)
+
+	require.HTTPBodyContains(t, handler.ServeHTTP, http.MethodGet, "/assets/index.txt", nil, "index")
+}
diff --git a/internal/oauth2/provider_test.go b/internal/oauth2/provider_test.go
index 487544e..71bbe9b 100644
--- a/internal/oauth2/provider_test.go
+++ b/internal/oauth2/provider_test.go
@@ -26,8 +26,6 @@ func TestNewProvider(t *testing.T) {
 	_, resourceServerURL, clientCredentials, err := testutils.SetupResourceServer(t, clientListener)
 	require.NoError(t, err)
 
-	logger := testutils.NewTestLogger()
-
 	tests := []struct {
 		name string
 		conf config.Config
@@ -135,6 +133,8 @@ func TestNewProvider(t *testing.T) {
 			ctx, cancel := context.WithCancel(t.Context())
 			t.Cleanup(cancel)
 
+			logger := testutils.NewTestLogger()
+
 			var (
 				err      error
 				provider oauth2.Provider
diff --git a/internal/utils/testutils/main.go b/internal/utils/testutils/main.go
index bc3c8a9..7075092 100644
--- a/internal/utils/testutils/main.go
+++ b/internal/utils/testutils/main.go
@@ -14,6 +14,7 @@ import (
 	"net/http/httptest"
 	"strings"
 	"testing"
+	"testing/fstest"
 	"time"
 	"unicode"
 
@@ -233,11 +234,10 @@ func SetupMockEnvironment(ctx context.Context, tb testing.TB, conf config.Config
 
 	oAuth2Client, openvpnClient := SetupOpenVPNOAuth2Clients(ctx, tb, conf, logger.Logger, httpClient, tokenStorage)
 
-	httpHandler, err := httphandler.New(conf, oAuth2Client)
-	require.NoError(tb, err)
-
+	httpHandler := httphandler.New(conf, oAuth2Client, fstest.MapFS{})
 	httpClientListener := httptest.NewUnstartedServer(httpHandler)
 	require.NoError(tb, httpClientListener.Listener.Close())
+
 	httpClientListener.Listener = clientListener
 	httpClientListener.Start()
 	tb.Cleanup(httpClientListener.Close)