Skip to content

Commit

Permalink
Refactor UI: Allow to override built-in assets and provide i18n capab…
Browse files Browse the repository at this point in the history
…ilities (#229)
  • Loading branch information
jkroepke authored Mar 29, 2024
1 parent 79ca847 commit 0bc7389
Show file tree
Hide file tree
Showing 18 changed files with 160 additions and 27 deletions.
16 changes: 4 additions & 12 deletions docs/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ debug:
pprof: false
listen: :9001
http:
assets-path: "" # Example: "/etc/openvpn-auth-oauth2/assets/"
baseurl: "http://localhost:9000/"
cert: ""
check:
Expand Down Expand Up @@ -117,6 +118,8 @@ Usage of openvpn-auth-oauth2:
listen address for go profiling endpoint (env: CONFIG_DEBUG_LISTEN) (default ":9001")
--debug.pprof
Enables go profiling endpoint. This should be never exposed. (env: CONFIG_DEBUG_PPROF)
--http.assets-path string
Custom path to the assets directory. Files in this directory will be served under /assets/ and having an higher priority than the embedded assets. (env: CONFIG_HTTP_ASSETS__PATH)
--http.baseurl string
listen addr for client listener (env: CONFIG_HTTP_BASEURL) (default "http://localhost:9000")
--http.cert string
Expand Down Expand Up @@ -336,18 +339,7 @@ openvpn-auth-oauth2 requires a [`SIGHUP` signal](https://en.wikipedia.org/wiki/S

## Custom Login Templates

openvpn-auth-oauth2 supports custom templates for the login page. The template must be a valid HTML file.

The default template is here:
[index.gohtml](https://github.com/jkroepke/openvpn-auth-oauth2/blob/main/internal/ui/index.gohtml)

Available variables:

- `{{.success}}`: Indicates if the login was successful
- `{{.title}}`: `Access Denied` or `Access Granted`
- `{{.message}}`: Potential error message or success message

The [go template engine](https://pkg.go.dev/text/template) is used to render the HTML file.
See [Layout Customization](Layout%20Customization) for more information

## Non-interactive session refresh

Expand Down
36 changes: 36 additions & 0 deletions docs/Layout Customization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Layout Customization

openvpn-auth-oauth2 supports custom templates for the login page. The template must be a valid HTML file.

The default template is here:
[index.gohtml](https://github.com/jkroepke/openvpn-auth-oauth2/blob/main/internal/ui/index.gohtml)

Available variables:

- `{{.title}}`: `Access denied` or `Access granted`
- `{{.message}}`: Potential error message or success message
- `{{.errorID}}`: ErrorID of an error, if present

The [go template engine](https://pkg.go.dev/text/template) is used to render the HTML file.

## Overriding the default assets

To override the default assets, you can configure `http.assets-path` with the path to the directory containing the assets.

The default assets are here:

- `style.css`: CSS file to enrich the default layout. By default, it is empty.
- `mvp.css`: [MVP](https://github.com/andybrewer/mvp) css framework
- `favicon.png`: Favicon of the login page
- `i18n.js`: Localization script
- `i18n/<lang>.json`: Language specific localization file. <lang> is the language code, e.g., `en` for English.
See [de.json](https://github.com/jkroepke/openvpn-auth-oauth2/blob/main/internal/ui/static/i18n/de.json) for an example.

## Custom localization

If you want to provide custom localization, you have to configure `http.assets-path` first. In the assets directory,
create a new directory named `i18n` and put your localization files in there. The file name must be the language code
followed by `.json`. For example, `en.json` for English.

Instead providing a custom localization file locally, think about to submit a pull request to the project to provide
the localization for everyone.
5 changes: 5 additions & 0 deletions internal/config/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,11 @@ func flagSetHTTP(flagSet *flag.FlagSet) {
Defaults.HTTP.EnableProxyHeaders,
"Use X-Forward-For http header for client ips",
)
flagSet.String(
"http.assets-path",
Defaults.HTTP.AssetsPath,
"Custom path to the assets directory. Files in this directory will be served under /assets/ and having an higher priority than the embedded assets.",
)
}

func flagSetOpenVPN(flagSet *flag.FlagSet) {
Expand Down
2 changes: 2 additions & 0 deletions internal/config/load_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ http:
listen: ":9001"
secret: "1jd93h5b6s82lf03jh5b2hf9"
enable-proxy-headers: true
assets-path: "."
check:
ipaddr: true
`,
Expand All @@ -143,6 +144,7 @@ http:
Scheme: "http",
Host: "localhost:9000",
},
AssetsPath: ".",
},
OpenVpn: config.OpenVpn{
Addr: &url.URL{
Expand Down
1 change: 1 addition & 0 deletions internal/config/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type HTTP struct {
CallbackTemplate *template.Template `koanf:"template"`
Check HTTPCheck `koanf:"check"`
EnableProxyHeaders bool `koanf:"enable-proxy-headers"`
AssetsPath string `koanf:"assets-path"`
}

type HTTPCheck struct {
Expand Down
7 changes: 7 additions & 0 deletions internal/config/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"net/url"
"os"
"slices"
)

Expand Down Expand Up @@ -75,5 +76,11 @@ func Validate(mode int, conf Config) error { //nolint:cyclop
}
}

if conf.HTTP.AssetsPath != "" {
if _, err := os.ReadDir(conf.HTTP.AssetsPath); err != nil {
return fmt.Errorf("http.assets-path: %w", err)
}
}

return nil
}
15 changes: 10 additions & 5 deletions internal/oauth2/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"log/slog"
"net"
"net/http"
"os"
"strconv"
"strings"
"time"
Expand All @@ -30,16 +31,20 @@ type OpenVPN interface {
}

func (p *Provider) Handler() *http.ServeMux {
staticFs, err := fs.Sub(ui.Static, "static")
staticFs, err := fs.Sub(ui.Static, "assets")
if err != nil {
panic(err)
}

if p.conf.HTTP.AssetsPath != "" {
staticFs = utils.NewOverlayFS(staticFs, os.DirFS(p.conf.HTTP.AssetsPath))
}

basePath := strings.TrimSuffix(p.conf.HTTP.BaseURL.Path, "/")

mux := http.NewServeMux()
mux.Handle("/", http.NotFoundHandler())
mux.Handle(fmt.Sprintf("GET %s/static/", basePath), http.StripPrefix(utils.StringConcat(basePath, "/static/"), http.FileServer(http.FS(staticFs))))
mux.Handle(fmt.Sprintf("GET %s/assets/", basePath), http.StripPrefix(utils.StringConcat(basePath, "/assets/"), http.FileServerFS(staticFs)))
mux.Handle(fmt.Sprintf("GET %s/oauth2/start", basePath), p.oauth2Start())
mux.Handle(fmt.Sprintf("GET %s/oauth2/callback", basePath), p.oauth2Callback())

Expand Down Expand Up @@ -260,8 +265,8 @@ func writeError(w http.ResponseWriter, logger *slog.Logger, conf config.Config,

err := conf.HTTP.CallbackTemplate.Execute(w, map[string]string{
"title": "Access denied",
"message": fmt.Sprintf("Error ID: %s\r\nPlease contact your administrator for help.", errorID),
"success": "false",
"message": "Please contact your administrator.",
"errorID": errorID,
})
if err != nil {
logger.Error("executing template:", err)
Expand All @@ -275,7 +280,7 @@ func writeSuccess(w http.ResponseWriter, conf config.Config, logger *slog.Logger
err := conf.HTTP.CallbackTemplate.Execute(w, map[string]string{
"title": "Access granted",
"message": "You can close this window now.",
"success": "true",
"errorID": "",
})
if err != nil {
logger.Error(fmt.Sprintf("executing template: %s", err))
Expand Down
File renamed without changes
12 changes: 12 additions & 0 deletions internal/ui/assets/i18n.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
(async () => {
const response = await fetch(`../assets/i18n/${navigator.language.split('-')[0]}.json`);
if (!response.ok) return

const json = await response.json();

document.querySelectorAll("[data-i18n]").forEach(el => {
if (el.innerText in json) {
el.innerText = json[el.innerText];
}
});
})();
6 changes: 6 additions & 0 deletions internal/ui/assets/i18n/de.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"Access denied": "Zugriff verweigert",
"Access granted": "Zugriff gewährt",
"Error ID": "Fehler ID",
"Please contact your administrator.": "Bitte kontaktieren Sie Ihren Administrator."
}
File renamed without changes.
Empty file added internal/ui/assets/style.css
Empty file.
2 changes: 1 addition & 1 deletion internal/ui/files.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package ui

import "embed"

//go:embed static/*
//go:embed assets/*
var Static embed.FS

//go:embed index.gohtml
Expand Down
19 changes: 11 additions & 8 deletions internal/ui/index.gohtml
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,30 @@
<html lang="en">
<head>
<meta http-equiv="Content-Security-Policy"
content="default-src 'none'; style-src 'self'; img-src 'self'; script-src 'sha256-7SYgPploBR3v25GCGZqhzo+r1EWDQ6bvZ+mn7tzpxOw='">
content="default-src 'none'; style-src 'self'; img-src 'self'; connect-src 'self'; script-src 'self' 'sha256-7SYgPploBR3v25GCGZqhzo+r1EWDQ6bvZ+mn7tzpxOw='">

<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>OpenVPN SSO Login</title>
<link rel="stylesheet" href="../static/mvp.css">
<link rel="icon" href="../static/favicon.png" sizes="192x192"/>
<link rel="stylesheet" href="../assets/mvp.css">
<link rel="stylesheet" href="../assets/style.css">
<link rel="icon" href="../assets/favicon.png" sizes="192x192"/>
</head>
<body>
<header>
<h1>{{ .title }}</h1>
<h1 id="title" data-i18n>{{ .title }}</h1>
</header>
<main>
<hr>
<section>
<h2>{{ .message }}</h2>
{{- if .errorID }}
<h2 id="errorID"><span data-i18n>Error ID</span>: {{ .errorID }}</h2>
{{- end }}
<h2 id="message" data-i18n>{{ .message }}</h2>
</section>
</main>
{{- if eq .success "true" }}
<script>setTimeout(window.close,10000)</script>
{{- end }}
<script src="../assets/i18n.js"></script>
{{- if not .errorID }}<script>setTimeout(window.close,10000)</script>{{ end }}
</body>
</html>
21 changes: 21 additions & 0 deletions internal/utils/overlayfs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package utils

import "io/fs"

type OverlayFS struct {
fs fs.FS
over fs.FS
}

func NewOverlayFS(fs, over fs.FS) *OverlayFS { return &OverlayFS{fs, over} }

func (f *OverlayFS) Open(name string) (fs.File, error) {
fi, err := fs.Stat(f.over, name)
if err == nil && !fi.IsDir() {
if f, err := f.over.Open(name); err == nil {
return f, nil
}
}

return f.fs.Open(name) //nolint:wrapcheck
}
42 changes: 42 additions & 0 deletions internal/utils/overlayfs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package utils_test

import (
"io/fs"
"testing"
"testing/fstest"

"github.com/jkroepke/openvpn-auth-oauth2/internal/utils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestNewOverlayFS(t *testing.T) {
t.Parallel()

baseFS := fstest.MapFS{
"base": &fstest.MapFile{
Data: []byte("base"),
},
"overlay": &fstest.MapFile{
Data: []byte("base"),
},
}
overlayFS := fstest.MapFS{
"overlay": &fstest.MapFile{
Data: []byte("overlay"),
},
}

ofs := utils.NewOverlayFS(baseFS, overlayFS)

content, err := fs.ReadFile(ofs, "overlay")
require.NoError(t, err)
assert.Equal(t, []byte("overlay"), content)

content, err = fs.ReadFile(ofs, "base")
require.NoError(t, err)
assert.Equal(t, []byte("base"), content)

_, err = fs.ReadFile(ofs, "nonexistent")
require.Error(t, err)
}
1 change: 1 addition & 0 deletions packaging/etc/openvpn-auth-oauth2/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# pprof: false
# listen: :9001
#http:
# assets-path: "" # Example: "/etc/openvpn-auth-oauth2/assets/"
# baseurl: "http://localhost:9000/"
# cert: ""
# check:
Expand Down
2 changes: 1 addition & 1 deletion tests/systemd/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM ubuntu:23.10
FROM debian:12

RUN groupadd openvpn-auth-oauth2
RUN apt update && apt install ca-certificates systemd openvpn apparmor apparmor-easyprof apparmor-profiles apparmor-profiles-extra -y

0 comments on commit 0bc7389

Please sign in to comment.