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
|
||||
## the system certificates store.
|
||||
## 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.
|
||||
theme: light
|
||||
|
@ -40,6 +40,10 @@ server:
|
|||
## Must be alphanumeric chars and should not contain any slashes.
|
||||
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.
|
||||
## Explanation at https://www.authelia.com/docs/configuration/server.html
|
||||
## Read buffer size adjusts the server's max incoming request size in bytes.
|
||||
|
|
|
@ -87,6 +87,33 @@ server:
|
|||
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
|
||||
<div markdown="1">
|
||||
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
|
||||
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.
|
||||
|
||||
### 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
|
||||
## the system certificates store.
|
||||
## 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.
|
||||
theme: light
|
||||
|
@ -40,6 +40,10 @@ server:
|
|||
## Must be alphanumeric chars and should not contain any slashes.
|
||||
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.
|
||||
## Explanation at https://www.authelia.com/docs/configuration/server.html
|
||||
## Read buffer size adjusts the server's max incoming request size in bytes.
|
||||
|
|
|
@ -5,6 +5,7 @@ type ServerConfiguration struct {
|
|||
Host string `koanf:"host"`
|
||||
Port int `koanf:"port"`
|
||||
Path string `koanf:"path"`
|
||||
AssetPath string `koanf:"asset_path"`
|
||||
ReadBufferSize int `koanf:"read_buffer_size"`
|
||||
WriteBufferSize int `koanf:"write_buffer_size"`
|
||||
EnablePprof bool `koanf:"enable_endpoint_pprof"`
|
||||
|
|
|
@ -147,6 +147,7 @@ var ValidKeys = []string{
|
|||
"server.read_buffer_size",
|
||||
"server.write_buffer_size",
|
||||
"server.path",
|
||||
"server.asset_path",
|
||||
"server.enable_pprof",
|
||||
"server.enable_expvars",
|
||||
"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 validOverrideAssets = []string{"favicon.ico", "logo.png"}
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
package server
|
||||
|
||||
const embeddedAssets = "public_html/"
|
||||
const swaggerAssets = embeddedAssets + "api/"
|
||||
const apiFile = "openapi.yml"
|
||||
const indexFile = "index.html"
|
||||
const (
|
||||
embeddedAssets = "public_html/"
|
||||
swaggerAssets = embeddedAssets + "api/"
|
||||
apiFile = "openapi.yml"
|
||||
indexFile = "index.html"
|
||||
logoFile = "logo.png"
|
||||
)
|
||||
|
||||
var rootFiles = []string{"favicon.ico", "manifest.json", "robots.txt"}
|
||||
|
||||
const dev = "dev"
|
||||
|
||||
|
|
|
@ -32,13 +32,12 @@ func registerRoutes(configuration schema.Configuration, providers middlewares.Pr
|
|||
|
||||
embeddedPath, _ := fs.Sub(assets, "public_html")
|
||||
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 != ""
|
||||
|
||||
serveIndexHandler := ServeTemplatedFile(embeddedAssets, indexFile, rememberMe, resetPassword, configuration.Session.Name, configuration.Theme, https)
|
||||
serveSwaggerHandler := ServeTemplatedFile(swaggerAssets, indexFile, rememberMe, resetPassword, configuration.Session.Name, configuration.Theme, https)
|
||||
serveSwaggerAPIHandler := ServeTemplatedFile(swaggerAssets, apiFile, 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, configuration.Server.AssetPath, 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.GET("/", serveIndexHandler)
|
||||
|
@ -48,10 +47,10 @@ func registerRoutes(configuration schema.Configuration, providers middlewares.Pr
|
|||
r.GET("/api/"+apiFile, serveSwaggerAPIHandler)
|
||||
|
||||
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.GET("/api/health", autheliaMiddleware(handlers.HealthGet))
|
||||
|
|
|
@ -16,7 +16,7 @@ import (
|
|||
// ServeTemplatedFile serves a templated version of a specified file,
|
||||
// this is utilised to pass information between the backend and frontend
|
||||
// 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()
|
||||
|
||||
f, err := assets.Open(publicDir + file)
|
||||
|
@ -40,6 +40,14 @@ func ServeTemplatedFile(publicDir, file, rememberMe, resetPassword, session, the
|
|||
base = baseURL.(string)
|
||||
}
|
||||
|
||||
logoOverride := "false"
|
||||
|
||||
if assetPath != "" {
|
||||
if _, err := os.Stat(assetPath + logoFile); err == nil {
|
||||
logoOverride = "true"
|
||||
}
|
||||
}
|
||||
|
||||
var scheme = "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))
|
||||
}
|
||||
|
||||
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 {
|
||||
ctx.Error("an error occurred", 503)
|
||||
logger.Errorf("Unable to execute template: %v", err)
|
||||
|
|
|
@ -7,6 +7,7 @@ jwt_secret: unsecure_secret
|
|||
|
||||
server:
|
||||
port: 9091
|
||||
asset_path: /config/assets/
|
||||
tls:
|
||||
certificate: /config/ssl/cert.pem
|
||||
key: /config/ssl/key.pem
|
||||
|
|
|
@ -7,6 +7,7 @@ jwt_secret: unsecure_secret
|
|||
|
||||
server:
|
||||
port: 9091
|
||||
asset_path: /config/assets/
|
||||
tls:
|
||||
certificate: /config/ssl/cert.pem
|
||||
key: /config/ssl/key.pem
|
||||
|
@ -46,6 +47,9 @@ access_control:
|
|||
- domain: "singlefactor.example.com"
|
||||
policy: one_factor
|
||||
|
||||
ntp:
|
||||
version: 3
|
||||
|
||||
notifier:
|
||||
smtp:
|
||||
host: smtp
|
||||
|
|
|
@ -5,5 +5,7 @@ services:
|
|||
volumes:
|
||||
- './Traefik2/configuration.yml:/config/configuration.yml:ro'
|
||||
- './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'
|
||||
...
|
||||
|
|
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_LOGO_OVERRIDE=false
|
||||
VITE_PUBLIC_URL=""
|
||||
VITE_REMEMBER_ME=true
|
||||
VITE_RESET_PASSWORD=true
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
VITE_LOGO_OVERRIDE={{.LogoOverride}}
|
||||
VITE_PUBLIC_URL={{.Base}}
|
||||
VITE_REMEMBER_ME={{.RememberMe}}
|
||||
VITE_RESET_PASSWORD={{.ResetPassword}}
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
<title>Login - Authelia</title>
|
||||
</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>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
|
|
|
@ -56,7 +56,7 @@
|
|||
"sourcemap": true
|
||||
}
|
||||
],
|
||||
"^.+\\.(css|svg)$": "jest-transform-stub"
|
||||
"^.+\\.(css|png|svg)$": "jest-transform-stub"
|
||||
},
|
||||
"transformIgnorePatterns": [
|
||||
"[/\\\\]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 { ReactComponent as UserSvg } from "@assets/images/user.svg";
|
||||
import { getLogoOverride } from "@utils/Configuration";
|
||||
|
||||
export interface Props {
|
||||
id?: string;
|
||||
|
@ -14,12 +15,17 @@ export interface Props {
|
|||
|
||||
const LoginLayout = function (props: Props) {
|
||||
const style = useStyles();
|
||||
const logo = getLogoOverride() ? (
|
||||
<img src="./static/media/logo.png" alt="Logo" className={style.icon} />
|
||||
) : (
|
||||
<UserSvg className={style.icon} />
|
||||
);
|
||||
return (
|
||||
<Grid id={props.id} className={style.root} container spacing={0} alignItems="center" justifyContent="center">
|
||||
<Container maxWidth="xs" className={style.rootContainer}>
|
||||
<Grid container>
|
||||
<Grid item xs={12}>
|
||||
<UserSvg className={style.icon}></UserSvg>
|
||||
{logo}
|
||||
</Grid>
|
||||
{props.title ? (
|
||||
<Grid item xs={12}>
|
||||
|
|
|
@ -7,6 +7,10 @@ export function getEmbeddedVariable(variableName: string) {
|
|||
return value;
|
||||
}
|
||||
|
||||
export function getLogoOverride() {
|
||||
return getEmbeddedVariable("logooverride") === "true";
|
||||
}
|
||||
|
||||
export function getRememberMe() {
|
||||
return getEmbeddedVariable("rememberme") === "true";
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue