feat: customizable static assets (#2597)
* feat: customizable static assets This change provides the means to override specific assets from the embedded Go FS with files situated on disk. We only allow overriding the following files currently: * favicon.ico * logo.png * refactor(server): make logo string a const * refactor(suites): override favicon and use ntp3 in traefik2 suite * test(suites): test logo override in traefik2 suite * test(suites): test asset override fallback in traefik suite Closes #1630.pull/2603/head
parent
417d421b9a
commit
0be883befb
|
@ -7,7 +7,7 @@
|
||||||
## Certificates directory specifies where Authelia will load trusted certificates (public portion) from in addition to
|
## Certificates directory specifies where Authelia will load trusted certificates (public portion) from in addition to
|
||||||
## the system certificates store.
|
## the system certificates store.
|
||||||
## They should be in base64 format, and have one of the following extensions: *.cer, *.crt, *.pem.
|
## They should be in base64 format, and have one of the following extensions: *.cer, *.crt, *.pem.
|
||||||
# certificates_directory: /config/certificates
|
# certificates_directory: /config/certificates/
|
||||||
|
|
||||||
## The theme to display: light, dark, grey, auto.
|
## The theme to display: light, dark, grey, auto.
|
||||||
theme: light
|
theme: light
|
||||||
|
@ -40,6 +40,10 @@ server:
|
||||||
## Must be alphanumeric chars and should not contain any slashes.
|
## Must be alphanumeric chars and should not contain any slashes.
|
||||||
path: ""
|
path: ""
|
||||||
|
|
||||||
|
## Set the path on disk to Authelia assets.
|
||||||
|
## Useful to allow overriding of specific static assets.
|
||||||
|
# asset_path: /config/assets/
|
||||||
|
|
||||||
## Buffers usually should be configured to be the same value.
|
## Buffers usually should be configured to be the same value.
|
||||||
## Explanation at https://www.authelia.com/docs/configuration/server.html
|
## Explanation at https://www.authelia.com/docs/configuration/server.html
|
||||||
## Read buffer size adjusts the server's max incoming request size in bytes.
|
## Read buffer size adjusts the server's max incoming request size in bytes.
|
||||||
|
|
|
@ -87,6 +87,33 @@ server:
|
||||||
path: authelia
|
path: authelia
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### asset_path
|
||||||
|
<div markdown="1">
|
||||||
|
type: string
|
||||||
|
{: .label .label-config .label-purple }
|
||||||
|
default: ""
|
||||||
|
{: .label .label-config .label-blue }
|
||||||
|
required: no
|
||||||
|
{: .label .label-config .label-green }
|
||||||
|
</div>
|
||||||
|
|
||||||
|
Authelia by default serves all static assets from an embedded filesystem in the Go binary.
|
||||||
|
|
||||||
|
Modifying this setting will allow you to override and serve specific assets for Authelia from a specified path.
|
||||||
|
All files that can be overridden are documented below and must be placed in the `asset_path` with a flat file structure.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```console
|
||||||
|
/config/assets/
|
||||||
|
├── favicon.ico
|
||||||
|
└── logo.png
|
||||||
|
```
|
||||||
|
|
||||||
|
|Asset |File name|
|
||||||
|
|:-----:|:---------------:|
|
||||||
|
|Favicon|favicon.ico |
|
||||||
|
|Logo |logo.png |
|
||||||
|
|
||||||
### read_buffer_size
|
### read_buffer_size
|
||||||
<div markdown="1">
|
<div markdown="1">
|
||||||
type: integer
|
type: integer
|
||||||
|
@ -189,3 +216,8 @@ The path to the public certificate for TLS connections. Must be in DER base64/PE
|
||||||
The read and write buffer sizes generally should be the same. This is because when Authelia verifies
|
The read and write buffer sizes generally should be the same. This is because when Authelia verifies
|
||||||
if the user is authorized to visit a URL, it also sends back nearly the same size response as the request. However
|
if the user is authorized to visit a URL, it also sends back nearly the same size response as the request. However
|
||||||
you're able to tune these individually depending on your needs.
|
you're able to tune these individually depending on your needs.
|
||||||
|
|
||||||
|
### Asset Overrides
|
||||||
|
|
||||||
|
If replacing the Logo for your Authelia portal it is recommended to upload a transparent PNG of your desired logo.
|
||||||
|
Authelia will automatically resize the logo to an appropriate size to present in the frontend.
|
|
@ -7,7 +7,7 @@
|
||||||
## Certificates directory specifies where Authelia will load trusted certificates (public portion) from in addition to
|
## Certificates directory specifies where Authelia will load trusted certificates (public portion) from in addition to
|
||||||
## the system certificates store.
|
## the system certificates store.
|
||||||
## They should be in base64 format, and have one of the following extensions: *.cer, *.crt, *.pem.
|
## They should be in base64 format, and have one of the following extensions: *.cer, *.crt, *.pem.
|
||||||
# certificates_directory: /config/certificates
|
# certificates_directory: /config/certificates/
|
||||||
|
|
||||||
## The theme to display: light, dark, grey, auto.
|
## The theme to display: light, dark, grey, auto.
|
||||||
theme: light
|
theme: light
|
||||||
|
@ -40,6 +40,10 @@ server:
|
||||||
## Must be alphanumeric chars and should not contain any slashes.
|
## Must be alphanumeric chars and should not contain any slashes.
|
||||||
path: ""
|
path: ""
|
||||||
|
|
||||||
|
## Set the path on disk to Authelia assets.
|
||||||
|
## Useful to allow overriding of specific static assets.
|
||||||
|
# asset_path: /config/assets/
|
||||||
|
|
||||||
## Buffers usually should be configured to be the same value.
|
## Buffers usually should be configured to be the same value.
|
||||||
## Explanation at https://www.authelia.com/docs/configuration/server.html
|
## Explanation at https://www.authelia.com/docs/configuration/server.html
|
||||||
## Read buffer size adjusts the server's max incoming request size in bytes.
|
## Read buffer size adjusts the server's max incoming request size in bytes.
|
||||||
|
|
|
@ -5,6 +5,7 @@ type ServerConfiguration struct {
|
||||||
Host string `koanf:"host"`
|
Host string `koanf:"host"`
|
||||||
Port int `koanf:"port"`
|
Port int `koanf:"port"`
|
||||||
Path string `koanf:"path"`
|
Path string `koanf:"path"`
|
||||||
|
AssetPath string `koanf:"asset_path"`
|
||||||
ReadBufferSize int `koanf:"read_buffer_size"`
|
ReadBufferSize int `koanf:"read_buffer_size"`
|
||||||
WriteBufferSize int `koanf:"write_buffer_size"`
|
WriteBufferSize int `koanf:"write_buffer_size"`
|
||||||
EnablePprof bool `koanf:"enable_endpoint_pprof"`
|
EnablePprof bool `koanf:"enable_endpoint_pprof"`
|
||||||
|
|
|
@ -147,6 +147,7 @@ var ValidKeys = []string{
|
||||||
"server.read_buffer_size",
|
"server.read_buffer_size",
|
||||||
"server.write_buffer_size",
|
"server.write_buffer_size",
|
||||||
"server.path",
|
"server.path",
|
||||||
|
"server.asset_path",
|
||||||
"server.enable_pprof",
|
"server.enable_pprof",
|
||||||
"server.enable_expvars",
|
"server.enable_expvars",
|
||||||
"server.disable_healthcheck",
|
"server.disable_healthcheck",
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
package middlewares
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/valyala/fasthttp"
|
||||||
|
|
||||||
|
"github.com/authelia/authelia/v4/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AssetOverrideMiddleware allows overriding and serving of specific embedded assets from disk.
|
||||||
|
func AssetOverrideMiddleware(assetPath string, next fasthttp.RequestHandler) fasthttp.RequestHandler {
|
||||||
|
return func(ctx *fasthttp.RequestCtx) {
|
||||||
|
uri := string(ctx.RequestURI())
|
||||||
|
file := uri[strings.LastIndex(uri, "/")+1:]
|
||||||
|
|
||||||
|
if assetPath != "" && utils.IsStringInSlice(file, validOverrideAssets) {
|
||||||
|
_, err := os.Stat(assetPath + file)
|
||||||
|
if err != nil {
|
||||||
|
next(ctx)
|
||||||
|
} else {
|
||||||
|
fasthttp.FSHandler(assetPath, strings.Count(uri, "/")-1)(ctx)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
next(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -29,3 +29,4 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
var protoHostSeparator = []byte("://")
|
var protoHostSeparator = []byte("://")
|
||||||
|
var validOverrideAssets = []string{"favicon.ico", "logo.png"}
|
||||||
|
|
|
@ -1,9 +1,14 @@
|
||||||
package server
|
package server
|
||||||
|
|
||||||
const embeddedAssets = "public_html/"
|
const (
|
||||||
const swaggerAssets = embeddedAssets + "api/"
|
embeddedAssets = "public_html/"
|
||||||
const apiFile = "openapi.yml"
|
swaggerAssets = embeddedAssets + "api/"
|
||||||
const indexFile = "index.html"
|
apiFile = "openapi.yml"
|
||||||
|
indexFile = "index.html"
|
||||||
|
logoFile = "logo.png"
|
||||||
|
)
|
||||||
|
|
||||||
|
var rootFiles = []string{"favicon.ico", "manifest.json", "robots.txt"}
|
||||||
|
|
||||||
const dev = "dev"
|
const dev = "dev"
|
||||||
|
|
||||||
|
|
|
@ -32,13 +32,12 @@ func registerRoutes(configuration schema.Configuration, providers middlewares.Pr
|
||||||
|
|
||||||
embeddedPath, _ := fs.Sub(assets, "public_html")
|
embeddedPath, _ := fs.Sub(assets, "public_html")
|
||||||
embeddedFS := fasthttpadaptor.NewFastHTTPHandler(http.FileServer(http.FS(embeddedPath)))
|
embeddedFS := fasthttpadaptor.NewFastHTTPHandler(http.FileServer(http.FS(embeddedPath)))
|
||||||
rootFiles := []string{"favicon.ico", "manifest.json", "robots.txt"}
|
|
||||||
|
|
||||||
https := configuration.Server.TLS.Key != "" && configuration.Server.TLS.Certificate != ""
|
https := configuration.Server.TLS.Key != "" && configuration.Server.TLS.Certificate != ""
|
||||||
|
|
||||||
serveIndexHandler := ServeTemplatedFile(embeddedAssets, indexFile, rememberMe, resetPassword, configuration.Session.Name, configuration.Theme, https)
|
serveIndexHandler := ServeTemplatedFile(embeddedAssets, indexFile, configuration.Server.AssetPath, rememberMe, resetPassword, configuration.Session.Name, configuration.Theme, https)
|
||||||
serveSwaggerHandler := ServeTemplatedFile(swaggerAssets, indexFile, rememberMe, resetPassword, configuration.Session.Name, configuration.Theme, https)
|
serveSwaggerHandler := ServeTemplatedFile(swaggerAssets, indexFile, configuration.Server.AssetPath, rememberMe, resetPassword, configuration.Session.Name, configuration.Theme, https)
|
||||||
serveSwaggerAPIHandler := ServeTemplatedFile(swaggerAssets, apiFile, rememberMe, resetPassword, configuration.Session.Name, configuration.Theme, https)
|
serveSwaggerAPIHandler := ServeTemplatedFile(swaggerAssets, apiFile, configuration.Server.AssetPath, rememberMe, resetPassword, configuration.Session.Name, configuration.Theme, https)
|
||||||
|
|
||||||
r := router.New()
|
r := router.New()
|
||||||
r.GET("/", serveIndexHandler)
|
r.GET("/", serveIndexHandler)
|
||||||
|
@ -48,10 +47,10 @@ func registerRoutes(configuration schema.Configuration, providers middlewares.Pr
|
||||||
r.GET("/api/"+apiFile, serveSwaggerAPIHandler)
|
r.GET("/api/"+apiFile, serveSwaggerAPIHandler)
|
||||||
|
|
||||||
for _, f := range rootFiles {
|
for _, f := range rootFiles {
|
||||||
r.GET("/"+f, embeddedFS)
|
r.GET("/"+f, middlewares.AssetOverrideMiddleware(configuration.Server.AssetPath, embeddedFS))
|
||||||
}
|
}
|
||||||
|
|
||||||
r.GET("/static/{filepath:*}", embeddedFS)
|
r.GET("/static/{filepath:*}", middlewares.AssetOverrideMiddleware(configuration.Server.AssetPath, embeddedFS))
|
||||||
r.ANY("/api/{filepath:*}", embeddedFS)
|
r.ANY("/api/{filepath:*}", embeddedFS)
|
||||||
|
|
||||||
r.GET("/api/health", autheliaMiddleware(handlers.HealthGet))
|
r.GET("/api/health", autheliaMiddleware(handlers.HealthGet))
|
||||||
|
|
|
@ -16,7 +16,7 @@ import (
|
||||||
// ServeTemplatedFile serves a templated version of a specified file,
|
// ServeTemplatedFile serves a templated version of a specified file,
|
||||||
// this is utilised to pass information between the backend and frontend
|
// this is utilised to pass information between the backend and frontend
|
||||||
// and generate a nonce to support a restrictive CSP while using material-ui.
|
// and generate a nonce to support a restrictive CSP while using material-ui.
|
||||||
func ServeTemplatedFile(publicDir, file, rememberMe, resetPassword, session, theme string, https bool) fasthttp.RequestHandler {
|
func ServeTemplatedFile(publicDir, file, assetPath, rememberMe, resetPassword, session, theme string, https bool) fasthttp.RequestHandler {
|
||||||
logger := logging.Logger()
|
logger := logging.Logger()
|
||||||
|
|
||||||
f, err := assets.Open(publicDir + file)
|
f, err := assets.Open(publicDir + file)
|
||||||
|
@ -40,6 +40,14 @@ func ServeTemplatedFile(publicDir, file, rememberMe, resetPassword, session, the
|
||||||
base = baseURL.(string)
|
base = baseURL.(string)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logoOverride := "false"
|
||||||
|
|
||||||
|
if assetPath != "" {
|
||||||
|
if _, err := os.Stat(assetPath + logoFile); err == nil {
|
||||||
|
logoOverride = "true"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var scheme = "https"
|
var scheme = "https"
|
||||||
|
|
||||||
if !https {
|
if !https {
|
||||||
|
@ -71,7 +79,7 @@ func ServeTemplatedFile(publicDir, file, rememberMe, resetPassword, session, the
|
||||||
ctx.Response.Header.Add("Content-Security-Policy", fmt.Sprintf("default-src 'self' ; object-src 'none'; style-src 'self' 'nonce-%s'", nonce))
|
ctx.Response.Header.Add("Content-Security-Policy", fmt.Sprintf("default-src 'self' ; object-src 'none'; style-src 'self' 'nonce-%s'", nonce))
|
||||||
}
|
}
|
||||||
|
|
||||||
err := tmpl.Execute(ctx.Response.BodyWriter(), struct{ Base, BaseURL, CSPNonce, RememberMe, ResetPassword, Session, Theme string }{Base: base, BaseURL: baseURL, CSPNonce: nonce, RememberMe: rememberMe, ResetPassword: resetPassword, Session: session, Theme: theme})
|
err := tmpl.Execute(ctx.Response.BodyWriter(), struct{ Base, BaseURL, CSPNonce, LogoOverride, RememberMe, ResetPassword, Session, Theme string }{Base: base, BaseURL: baseURL, CSPNonce: nonce, LogoOverride: logoOverride, RememberMe: rememberMe, ResetPassword: resetPassword, Session: session, Theme: theme})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Error("an error occurred", 503)
|
ctx.Error("an error occurred", 503)
|
||||||
logger.Errorf("Unable to execute template: %v", err)
|
logger.Errorf("Unable to execute template: %v", err)
|
||||||
|
|
|
@ -7,6 +7,7 @@ jwt_secret: unsecure_secret
|
||||||
|
|
||||||
server:
|
server:
|
||||||
port: 9091
|
port: 9091
|
||||||
|
asset_path: /config/assets/
|
||||||
tls:
|
tls:
|
||||||
certificate: /config/ssl/cert.pem
|
certificate: /config/ssl/cert.pem
|
||||||
key: /config/ssl/key.pem
|
key: /config/ssl/key.pem
|
||||||
|
|
|
@ -7,6 +7,7 @@ jwt_secret: unsecure_secret
|
||||||
|
|
||||||
server:
|
server:
|
||||||
port: 9091
|
port: 9091
|
||||||
|
asset_path: /config/assets/
|
||||||
tls:
|
tls:
|
||||||
certificate: /config/ssl/cert.pem
|
certificate: /config/ssl/cert.pem
|
||||||
key: /config/ssl/key.pem
|
key: /config/ssl/key.pem
|
||||||
|
@ -46,6 +47,9 @@ access_control:
|
||||||
- domain: "singlefactor.example.com"
|
- domain: "singlefactor.example.com"
|
||||||
policy: one_factor
|
policy: one_factor
|
||||||
|
|
||||||
|
ntp:
|
||||||
|
version: 3
|
||||||
|
|
||||||
notifier:
|
notifier:
|
||||||
smtp:
|
smtp:
|
||||||
host: smtp
|
host: smtp
|
||||||
|
|
|
@ -5,5 +5,7 @@ services:
|
||||||
volumes:
|
volumes:
|
||||||
- './Traefik2/configuration.yml:/config/configuration.yml:ro'
|
- './Traefik2/configuration.yml:/config/configuration.yml:ro'
|
||||||
- './Traefik2/users.yml:/config/users.yml'
|
- './Traefik2/users.yml:/config/users.yml'
|
||||||
|
- './Traefik2/favicon.ico:/config/assets/favicon.ico'
|
||||||
|
- './Traefik2/logo.png:/config/assets/logo.png'
|
||||||
- './common/ssl:/config/ssl:ro'
|
- './common/ssl:/config/ssl:ro'
|
||||||
...
|
...
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 4.4 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
|
@ -1,4 +1,5 @@
|
||||||
VITE_HMR_PORT=8080
|
VITE_HMR_PORT=8080
|
||||||
|
VITE_LOGO_OVERRIDE=false
|
||||||
VITE_PUBLIC_URL=""
|
VITE_PUBLIC_URL=""
|
||||||
VITE_REMEMBER_ME=true
|
VITE_REMEMBER_ME=true
|
||||||
VITE_RESET_PASSWORD=true
|
VITE_RESET_PASSWORD=true
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
VITE_LOGO_OVERRIDE={{.LogoOverride}}
|
||||||
VITE_PUBLIC_URL={{.Base}}
|
VITE_PUBLIC_URL={{.Base}}
|
||||||
VITE_REMEMBER_ME={{.RememberMe}}
|
VITE_REMEMBER_ME={{.RememberMe}}
|
||||||
VITE_RESET_PASSWORD={{.ResetPassword}}
|
VITE_RESET_PASSWORD={{.ResetPassword}}
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
<title>Login - Authelia</title>
|
<title>Login - Authelia</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body data-basepath="%VITE_PUBLIC_URL%" data-rememberme="%VITE_REMEMBER_ME%" data-resetpassword="%VITE_RESET_PASSWORD%" data-theme="%VITE_THEME%">
|
<body data-basepath="%VITE_PUBLIC_URL%" data-logooverride="%VITE_LOGO_OVERRIDE%" data-rememberme="%VITE_REMEMBER_ME%" data-resetpassword="%VITE_RESET_PASSWORD%" data-theme="%VITE_THEME%">
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/index.tsx"></script>
|
<script type="module" src="/src/index.tsx"></script>
|
||||||
|
|
|
@ -56,7 +56,7 @@
|
||||||
"sourcemap": true
|
"sourcemap": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"^.+\\.(css|svg)$": "jest-transform-stub"
|
"^.+\\.(css|png|svg)$": "jest-transform-stub"
|
||||||
},
|
},
|
||||||
"transformIgnorePatterns": [
|
"transformIgnorePatterns": [
|
||||||
"[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs|cjs|ts|tsx)$"
|
"[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs|cjs|ts|tsx)$"
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { Grid, makeStyles, Container, Typography, Link } from "@material-ui/core
|
||||||
import { grey } from "@material-ui/core/colors";
|
import { grey } from "@material-ui/core/colors";
|
||||||
|
|
||||||
import { ReactComponent as UserSvg } from "@assets/images/user.svg";
|
import { ReactComponent as UserSvg } from "@assets/images/user.svg";
|
||||||
|
import { getLogoOverride } from "@utils/Configuration";
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
id?: string;
|
id?: string;
|
||||||
|
@ -14,12 +15,17 @@ export interface Props {
|
||||||
|
|
||||||
const LoginLayout = function (props: Props) {
|
const LoginLayout = function (props: Props) {
|
||||||
const style = useStyles();
|
const style = useStyles();
|
||||||
|
const logo = getLogoOverride() ? (
|
||||||
|
<img src="./static/media/logo.png" alt="Logo" className={style.icon} />
|
||||||
|
) : (
|
||||||
|
<UserSvg className={style.icon} />
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<Grid id={props.id} className={style.root} container spacing={0} alignItems="center" justifyContent="center">
|
<Grid id={props.id} className={style.root} container spacing={0} alignItems="center" justifyContent="center">
|
||||||
<Container maxWidth="xs" className={style.rootContainer}>
|
<Container maxWidth="xs" className={style.rootContainer}>
|
||||||
<Grid container>
|
<Grid container>
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<UserSvg className={style.icon}></UserSvg>
|
{logo}
|
||||||
</Grid>
|
</Grid>
|
||||||
{props.title ? (
|
{props.title ? (
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
|
|
|
@ -7,6 +7,10 @@ export function getEmbeddedVariable(variableName: string) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getLogoOverride() {
|
||||||
|
return getEmbeddedVariable("logooverride") === "true";
|
||||||
|
}
|
||||||
|
|
||||||
export function getRememberMe() {
|
export function getRememberMe() {
|
||||||
return getEmbeddedVariable("rememberme") === "true";
|
return getEmbeddedVariable("rememberme") === "true";
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue