mirror of
https://github.com/h44z/wg-portal.git
synced 2026-02-24 19:36:21 +00:00
Compare commits
2 Commits
doc_update
...
custom_tem
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee454c5d60 | ||
|
|
fb607a26b7 |
@@ -84,7 +84,7 @@ func main() {
|
||||
internal.AssertNoError(err)
|
||||
userManager.StartBackgroundJobs(ctx)
|
||||
|
||||
authenticator, err := auth.NewAuthenticator(&cfg.Auth, cfg.Web.ExternalUrl, cfg.Web.BasePath, eventBus, userManager)
|
||||
authenticator, err := auth.NewAuthenticator(&cfg.Auth, cfg.Web.ExternalUrl, eventBus, userManager)
|
||||
internal.AssertNoError(err)
|
||||
authenticator.StartBackgroundJobs(ctx)
|
||||
|
||||
|
||||
@@ -11,11 +11,18 @@ core:
|
||||
|
||||
web:
|
||||
external_url: http://localhost:8888
|
||||
base_path: ""
|
||||
request_logging: true
|
||||
# Optional path where custom frontend files are stored.
|
||||
# If this folder contains at least one file, it will override the embedded frontend.
|
||||
# If the folder is empty or does not exist on startup, the embedded frontend will be
|
||||
# written into it. Leave empty to use the embedded frontend only.
|
||||
frontend_filepath: ""
|
||||
|
||||
mail:
|
||||
# Path where custom email templates (.gotpl and .gohtml) are stored.
|
||||
# If the directory is empty on startup, the default embedded templates
|
||||
# will be written there so you can modify them.
|
||||
# Leave empty to use embedded templates only.
|
||||
templates_path: ""
|
||||
|
||||
webhook:
|
||||
|
||||
@@ -88,7 +88,6 @@ auth:
|
||||
web:
|
||||
listening_address: :8888
|
||||
external_url: http://localhost:8888
|
||||
base_path: ""
|
||||
site_company_name: WireGuard Portal
|
||||
site_title: WireGuard Portal
|
||||
session_identifier: wgPortalSession
|
||||
@@ -801,16 +800,9 @@ Without a valid `external_url`, the login process may fail due to CSRF protectio
|
||||
### `external_url`
|
||||
- **Default:** `http://localhost:8888`
|
||||
- **Environment Variable:** `WG_PORTAL_WEB_EXTERNAL_URL`
|
||||
- **Description:** The URL where a client can access WireGuard Portal. This URL is used for generating links in emails and for performing OAUTH redirects.
|
||||
The external URL must not contain a path component or trailing slash. If you want to serve WireGuard Portal on a subpath, use the `base_path` setting.
|
||||
- **Description:** The URL where a client can access WireGuard Portal. This URL is used for generating links in emails and for performing OAUTH redirects.
|
||||
**Important:** If you are using a reverse proxy, set this to the external URL of the reverse proxy, otherwise login will fail. If you access the portal via IP address, set this to the IP address of the server.
|
||||
|
||||
### `base_path`
|
||||
- **Default:** *(empty)*
|
||||
- **Environment Variable:** `WG_PORTAL_WEB_BASE_PATH`
|
||||
- **Description:** The base path for the web server (e.g., `/wgportal`).
|
||||
By default (meaning an empty value), the portal will be served from the root path `/`.
|
||||
|
||||
### `site_company_name`
|
||||
- **Default:** `WireGuard Portal`
|
||||
- **Environment Variable:** `WG_PORTAL_WEB_SITE_COMPANY_NAME`
|
||||
|
||||
@@ -9,11 +9,6 @@ Make sure that you download the correct binary for your architecture. The availa
|
||||
- `wg-portal_linux_arm64` - Linux ARM 64-bit
|
||||
- `wg-portal_linux_arm_v7` - Linux ARM 32-bit
|
||||
|
||||
### Released versions
|
||||
|
||||
To download a specific version, replace `${WG_PORTAL_VERSION}` with the desired version (or set an environment variable).
|
||||
All official release versions can be found on the [GitHub Releases Page](https://github.com/h44z/wg-portal/releases).
|
||||
|
||||
With `curl`:
|
||||
|
||||
```shell
|
||||
@@ -32,74 +27,16 @@ with `gh cli`:
|
||||
gh release download ${WG_PORTAL_VERSION} --repo h44z/wg-portal --output wg-portal --pattern '*amd64'
|
||||
```
|
||||
|
||||
The downloaded file will be named `wg-portal` and can be moved to a directory of your choice, see [Install](#install) for more information.
|
||||
|
||||
### Unreleased versions (master branch builds)
|
||||
|
||||
Unreleased versions can be fetched directly from the artifacts section of the [GitHub Workflow](https://github.com/h44z/wg-portal/actions/workflows/docker-publish.yml?query=branch%3Amaster).
|
||||
|
||||
|
||||
## Install
|
||||
|
||||
The following command can be used to install the downloaded binary (`wg-portal`) to `/opt/wg-portal/wg-portal`. It ensures that the binary is executable.
|
||||
|
||||
```shell
|
||||
sudo mkdir -p /opt/wg-portal
|
||||
sudo install wg-portal /opt/wg-portal/
|
||||
```
|
||||
|
||||
To handle tasks such as restarting the service or configuring automatic startup, it is recommended to use a process manager like [systemd](https://systemd.io/).
|
||||
Refer to [Systemd Service Setup](#systemd-service-setup) for instructions.
|
||||
## Unreleased versions (master branch builds)
|
||||
|
||||
## Systemd Service Setup
|
||||
Unreleased versions can be fetched directly from the artifacts section of the [GitHub Workflow](https://github.com/h44z/wg-portal/actions/workflows/docker-publish.yml?query=branch%3Amaster).
|
||||
|
||||
> **Note:** To run WireGuard Portal as systemd service, you need to download the binary for your architecture beforehand.
|
||||
>
|
||||
> The following examples assume that you downloaded the binary to `/opt/wg-portal/wg-portal`.
|
||||
> The configuration file is expected to be located at `/opt/wg-portal/config.yml`.
|
||||
|
||||
To run WireGuard Portal as a systemd service, you can create a service unit file. The easiest way to do this is by using `systemctl edit`:
|
||||
|
||||
```shell
|
||||
sudo systemctl edit --force --full wg-portal.service
|
||||
```
|
||||
|
||||
Paste the following content into the editor and adjust the variables to your needs:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=WireGuard Portal
|
||||
ConditionPathExists=/opt/wg-portal/wg-portal
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
Group=root
|
||||
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW
|
||||
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
|
||||
WorkingDirectory=/opt/wg-portal
|
||||
Environment=WG_PORTAL_CONFIG=/opt/wg-portal/config.yml
|
||||
ExecStart=/opt/wg-portal/wg-portal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
Alternatively, you can create or modify the file manually in `/etc/systemd/system/wg-portal.service`.
|
||||
For systemd to pick up the changes, you need to reload the daemon:
|
||||
|
||||
```shell
|
||||
sudo systemctl daemon-reload
|
||||
```
|
||||
|
||||
After creating the service file, you can enable and start the service:
|
||||
|
||||
```shell
|
||||
sudo systemctl enable --now wg-portal.service
|
||||
```
|
||||
|
||||
To check status and log output, use: `sudo systemctl status wg-portal.service` or `sudo journalctl -u wg-portal.service`.
|
||||
|
||||
@@ -84,16 +84,6 @@ web:
|
||||
external_url: https://wg.domain.com
|
||||
```
|
||||
|
||||
If you want to serve the web interface on a different base-path, you can also set the `web.base_path` option:
|
||||
|
||||
```yaml
|
||||
web:
|
||||
external_url: https://wg.domain.com
|
||||
base_path: /subpath
|
||||
```
|
||||
|
||||
The WireGuard Portal will then be available at `https://wg.domain.com/subpath`.
|
||||
|
||||
### Built-in TLS
|
||||
|
||||
If you prefer to let WireGuard Portal handle TLS itself, you can use the built-in TLS support.
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
<script>
|
||||
// global config, will be overridden by backend if available
|
||||
let WGPORTAL_BACKEND_BASE_URL="http://localhost:5000/api/v0";
|
||||
let WGPORTAL_BASE_PATH="";
|
||||
let WGPORTAL_VERSION="unknown";
|
||||
let WGPORTAL_SITE_TITLE="WireGuard Portal";
|
||||
let WGPORTAL_SITE_COMPANY_NAME="WireGuard Portal";
|
||||
|
||||
@@ -85,7 +85,6 @@ const languageFlag = computed(() => {
|
||||
const companyName = ref(WGPORTAL_SITE_COMPANY_NAME);
|
||||
const wgVersion = ref(WGPORTAL_VERSION);
|
||||
const currentYear = ref(new Date().getFullYear())
|
||||
const webBasePath = ref(WGPORTAL_BASE_PATH);
|
||||
|
||||
const userDisplayName = computed(() => {
|
||||
let displayName = "Unknown";
|
||||
@@ -114,7 +113,7 @@ const userDisplayName = computed(() => {
|
||||
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
|
||||
<div class="container-fluid">
|
||||
<RouterLink class="navbar-brand" :to="{ name: 'home' }"><img :alt="companyName" :src="webBasePath + '/img/header-logo.png'" /></RouterLink>
|
||||
<a class="navbar-brand" href="/"><img :alt="companyName" src="/img/header-logo.png" /></a>
|
||||
<button aria-controls="navbarColor01" aria-expanded="false" aria-label="Toggle navigation" class="navbar-toggler"
|
||||
data-bs-target="#navbarTop" data-bs-toggle="collapse" type="button">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
|
||||
@@ -9,8 +9,6 @@ const profile = profileStore()
|
||||
const settings = settingsStore()
|
||||
const auth = authStore()
|
||||
|
||||
const webBasePath = ref(WGPORTAL_BASE_PATH);
|
||||
|
||||
onMounted(async () => {
|
||||
await profile.LoadUser()
|
||||
await auth.LoadWebAuthnCredentials()
|
||||
@@ -243,7 +241,7 @@ const updatePassword = async () => {
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<a :href="webBasePath + '/api/v1/doc.html'" target="_blank" :alt="$t('settings.api.api-link')">{{ $t('settings.api.api-link') }}</a>
|
||||
<a href="/api/v1/doc.html" target="_blank" :alt="$t('settings.api.api-link')">{{ $t('settings.api.api-link') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
4
go.mod
4
go.mod
@@ -9,7 +9,7 @@ require (
|
||||
github.com/glebarez/sqlite v1.11.0
|
||||
github.com/go-ldap/ldap/v3 v3.4.12
|
||||
github.com/go-pkgz/routegroup v1.6.0
|
||||
github.com/go-playground/validator/v10 v10.30.1
|
||||
github.com/go-playground/validator/v10 v10.28.0
|
||||
github.com/go-webauthn/webauthn v0.15.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/prometheus-community/pro-bing v0.7.0
|
||||
@@ -41,7 +41,7 @@ require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.11 // indirect
|
||||
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
||||
|
||||
8
go.sum
8
go.sum
@@ -50,8 +50,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik=
|
||||
github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
|
||||
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
|
||||
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
||||
@@ -97,8 +97,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
|
||||
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
|
||||
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 709 B After Width: | Height: | Size: 7.5 KiB |
@@ -6,9 +6,9 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||||
<title>WireGuard Portal API</title>
|
||||
<meta name="description" content="WireGuard Portal API">
|
||||
<link rel="stylesheet" href="{{$.BasePath}}/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="{{$.BasePath}}/fonts/fontawesome-all.min.css">
|
||||
<link rel="shortcut icon" type="image/x-icon" href="{{$.BasePath}}/app/favicon.ico">
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/fonts/fontawesome-all.min.css">
|
||||
<link rel="shortcut icon" type="image/x-icon" href="/app/favicon.ico">
|
||||
</head>
|
||||
|
||||
<body id="page-top" class="d-flex flex-column min-vh-100">
|
||||
@@ -25,7 +25,7 @@
|
||||
<div class="card-header">SPA Api</div>
|
||||
<div class="card-body">
|
||||
<p class="card-text">This endpoint is used by the Single Page Application (the Frontend/UI).</p>
|
||||
<a href="{{$.BasePath}}/api/v0/doc.html" title="API version 0" target="_blank" rel="noopener noreferrer" class="btn btn-primary btn-sm">Open Documentation</a>
|
||||
<a href="/api/v0/doc.html" title="API version 0" target="_blank" rel="noopener noreferrer" class="btn btn-primary btn-sm">Open Documentation</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -34,7 +34,7 @@
|
||||
<div class="card-header"><b>Version 1</b></div>
|
||||
<div class="card-body">
|
||||
<p class="card-text">This is the current main API endpoint.</p>
|
||||
<a href="{{$.BasePath}}/api/v1/doc.html" title="API version 1" target="_blank" rel="noopener noreferrer" class="btn btn-primary btn-sm">Open Documentation</a>
|
||||
<a href="/api/v1/doc.html" title="API version 1" target="_blank" rel="noopener noreferrer" class="btn btn-primary btn-sm">Open Documentation</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -43,17 +43,17 @@
|
||||
<div class="card-header">Version 2</div>
|
||||
<div class="card-body">
|
||||
<p class="card-text">This will be a future API version, it is currently work in progress.</p>
|
||||
<a href="{{$.BasePath}}/api/v2/doc.html" title="API version 2" target="_blank" rel="noopener noreferrer" class="btn btn-primary btn-sm">Open Documentation</a>
|
||||
<a href="/api/v2/doc.html" title="API version 2" target="_blank" rel="noopener noreferrer" class="btn btn-primary btn-sm">Open Documentation</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{template "prt_footer.gohtml" .}}
|
||||
<script src="{{$.BasePath}}/js/jquery.min.js"></script>
|
||||
<script src="{{$.BasePath}}/js/jquery.easing.js"></script>
|
||||
<script src="{{$.BasePath}}/js/popper.min.js"></script>
|
||||
<script src="{{$.BasePath}}/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/js/jquery.min.js"></script>
|
||||
<script src="/js/jquery.easing.js"></script>
|
||||
<script src="/js/popper.min.js"></script>
|
||||
<script src="/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -3,7 +3,7 @@
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<a class="navbar-brand" href="/"><img src="{{$.BasePath}}/img/header-logo.png" alt="Prolicht"/></a>
|
||||
<a class="navbar-brand" href="/"><img src="/img/header-logo.png" alt="Prolicht"/></a>
|
||||
<div id="topNavbar" class="navbar-collapse collapse">
|
||||
<ul class="navbar-nav mr-auto mt-2 mt-lg-0">
|
||||
</ul>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
allow-spec-file-load="false"
|
||||
allow-spec-file-download="true"
|
||||
>
|
||||
<img slot="logo" src="{{$.BasePath}}/img/header-logo-small.png" style="width:50px; height:50px"/>
|
||||
<img slot="logo" src="/img/header-logo-small.png" style="width:50px; height:50px"/>
|
||||
|
||||
<p slot="footer" style="margin:0; padding:16px 36px; background-color:#f76b39; color:#fff; text-align:center;" >
|
||||
Copyright © WireGuard Portal {{$.Year}}, version {{$.Version}}
|
||||
|
||||
@@ -10,8 +10,6 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-pkgz/routegroup"
|
||||
@@ -39,7 +37,6 @@ type ApiEndpointSetupFunc func() (ApiVersion, GroupSetupFn)
|
||||
type Server struct {
|
||||
cfg *config.Config
|
||||
server *routegroup.Bundle
|
||||
root *routegroup.Bundle // root is the web-root (potentially with path prefix)
|
||||
tpl *respond.TemplateRenderer
|
||||
versions map[ApiVersion]*routegroup.Bundle
|
||||
}
|
||||
@@ -80,42 +77,13 @@ func NewServer(cfg *config.Config, endpoints ...ApiEndpointSetupFunc) (*Server,
|
||||
template.Must(template.New("").ParseFS(apiTemplates, "assets/tpl/*.gohtml")),
|
||||
)
|
||||
|
||||
// Mount base path if configured
|
||||
s.root = s.server
|
||||
if s.cfg.Web.BasePath != "" {
|
||||
s.root = s.server.Mount(s.cfg.Web.BasePath)
|
||||
}
|
||||
|
||||
// Serve static files (under base path if configured)
|
||||
// Serve static files
|
||||
imgFs := http.FS(fsMust(fs.Sub(apiStatics, "assets/img")))
|
||||
s.root.HandleFiles("/css", http.FS(fsMust(fs.Sub(apiStatics, "assets/css"))))
|
||||
s.root.HandleFiles("/js", http.FS(fsMust(fs.Sub(apiStatics, "assets/js"))))
|
||||
s.root.HandleFiles("/img", imgFs)
|
||||
s.root.HandleFiles("/fonts", http.FS(fsMust(fs.Sub(apiStatics, "assets/fonts"))))
|
||||
if cfg.Web.BasePath == "" {
|
||||
s.root.HandleFiles("/doc", http.FS(fsMust(fs.Sub(apiStatics, "assets/doc"))))
|
||||
} else {
|
||||
customV0File, _ := fs.ReadFile(fsMust(fs.Sub(apiStatics, "assets/doc")), "v0_swagger.yaml")
|
||||
customV1File, _ := fs.ReadFile(fsMust(fs.Sub(apiStatics, "assets/doc")), "v1_swagger.yaml")
|
||||
customV0File = []byte(strings.Replace(string(customV0File),
|
||||
"basePath: /api/v0", "basePath: "+cfg.Web.BasePath+"/api/v0", 1))
|
||||
customV1File = []byte(strings.Replace(string(customV1File),
|
||||
"basePath: /api/v1", "basePath: "+cfg.Web.BasePath+"/api/v1", 1))
|
||||
|
||||
s.root.HandleFunc("GET /doc/", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == s.cfg.Web.BasePath+"/doc/v0_swagger.yaml" {
|
||||
respond.Data(w, http.StatusOK, "application/yaml", customV0File)
|
||||
return
|
||||
}
|
||||
|
||||
if r.URL.Path == s.cfg.Web.BasePath+"/doc/v1_swagger.yaml" {
|
||||
respond.Data(w, http.StatusOK, "application/yaml", customV1File)
|
||||
return
|
||||
}
|
||||
|
||||
respond.Status(w, http.StatusNotFound)
|
||||
})
|
||||
}
|
||||
s.server.HandleFiles("/css", http.FS(fsMust(fs.Sub(apiStatics, "assets/css"))))
|
||||
s.server.HandleFiles("/js", http.FS(fsMust(fs.Sub(apiStatics, "assets/js"))))
|
||||
s.server.HandleFiles("/img", imgFs)
|
||||
s.server.HandleFiles("/fonts", http.FS(fsMust(fs.Sub(apiStatics, "assets/fonts"))))
|
||||
s.server.HandleFiles("/doc", http.FS(fsMust(fs.Sub(apiStatics, "assets/doc"))))
|
||||
|
||||
// Setup routes
|
||||
s.setupRoutes(endpoints...)
|
||||
@@ -160,14 +128,14 @@ func (s *Server) Run(ctx context.Context, listenAddress string) {
|
||||
}
|
||||
|
||||
func (s *Server) setupRoutes(endpoints ...ApiEndpointSetupFunc) {
|
||||
s.root.HandleFunc("GET /api", s.landingPage)
|
||||
s.server.HandleFunc("GET /api", s.landingPage)
|
||||
s.versions = make(map[ApiVersion]*routegroup.Bundle)
|
||||
|
||||
for _, setupFunc := range endpoints {
|
||||
version, groupSetupFn := setupFunc()
|
||||
|
||||
if _, ok := s.versions[version]; !ok {
|
||||
s.versions[version] = s.root.Mount(fmt.Sprintf("/api/%s", version))
|
||||
s.versions[version] = s.server.Mount(fmt.Sprintf("/api/%s", version))
|
||||
|
||||
// OpenAPI documentation (via RapiDoc)
|
||||
s.versions[version].HandleFunc("GET /swagger/index.html", s.rapiDocHandler(version)) // Deprecated: old link
|
||||
@@ -181,17 +149,16 @@ func (s *Server) setupRoutes(endpoints ...ApiEndpointSetupFunc) {
|
||||
|
||||
func (s *Server) setupFrontendRoutes() {
|
||||
// Serve static files
|
||||
s.root.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
respond.Redirect(w, r, http.StatusMovedPermanently, s.cfg.Web.BasePath+"/app")
|
||||
s.server.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
respond.Redirect(w, r, http.StatusMovedPermanently, "/app")
|
||||
})
|
||||
|
||||
s.root.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
|
||||
respond.Redirect(w, r, http.StatusMovedPermanently, s.cfg.Web.BasePath+"/app/favicon.ico")
|
||||
s.server.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
|
||||
respond.Redirect(w, r, http.StatusMovedPermanently, "/app/favicon.ico")
|
||||
})
|
||||
|
||||
// If a custom frontend path is configured, serve files from there when it contains content.
|
||||
// If the directory is empty or missing, populate it with the embedded frontend-dist content first.
|
||||
useEmbeddedFrontend := true
|
||||
if s.cfg.Web.FrontendFilePath != "" {
|
||||
if err := os.MkdirAll(s.cfg.Web.FrontendFilePath, 0755); err != nil {
|
||||
slog.Error("failed to create frontend base directory", "path", s.cfg.Web.FrontendFilePath, "error", err)
|
||||
@@ -214,93 +181,34 @@ func (s *Server) setupFrontendRoutes() {
|
||||
if ok {
|
||||
// serve files from FS
|
||||
slog.Debug("serving frontend files from custom path", "path", s.cfg.Web.FrontendFilePath)
|
||||
useEmbeddedFrontend = false
|
||||
s.server.HandleFiles("/app", http.Dir(s.cfg.Web.FrontendFilePath))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var fileServer http.Handler
|
||||
if useEmbeddedFrontend {
|
||||
fileServer = http.FileServer(http.FS(fsMust(fs.Sub(frontendStatics, "frontend-dist"))))
|
||||
} else {
|
||||
fileServer = http.FileServer(http.Dir(s.cfg.Web.FrontendFilePath))
|
||||
}
|
||||
fileServer = http.StripPrefix(s.cfg.Web.BasePath+"/app", fileServer)
|
||||
|
||||
// Modify index.html and CSS to include the correct base path.
|
||||
var customIndexFile, customCssFile []byte
|
||||
var customCssFileName string
|
||||
if s.cfg.Web.BasePath != "" {
|
||||
customIndexFile, customCssFile, customCssFileName = s.updateBasePathInFrontend(useEmbeddedFrontend)
|
||||
}
|
||||
|
||||
s.root.HandleFunc("GET /app/", func(w http.ResponseWriter, r *http.Request) {
|
||||
// serve a custom index.html file with the correct base path applied
|
||||
if s.cfg.Web.BasePath != "" && r.URL.Path == s.cfg.Web.BasePath+"/app/" {
|
||||
respond.Data(w, http.StatusOK, "text/html", customIndexFile)
|
||||
return
|
||||
}
|
||||
|
||||
// serve a custom CSS file with the correct base path applied
|
||||
if s.cfg.Web.BasePath != "" && r.URL.Path == s.cfg.Web.BasePath+"/app/assets/"+customCssFileName {
|
||||
respond.Data(w, http.StatusOK, "text/css", customCssFile)
|
||||
return
|
||||
}
|
||||
|
||||
// pass all other requests to the file server
|
||||
fileServer.ServeHTTP(w, r)
|
||||
})
|
||||
// Fallback: serve embedded frontend files
|
||||
s.server.HandleFiles("/app", http.FS(fsMust(fs.Sub(frontendStatics, "frontend-dist"))))
|
||||
}
|
||||
|
||||
func (s *Server) landingPage(w http.ResponseWriter, _ *http.Request) {
|
||||
s.tpl.HTML(w, http.StatusOK, "index.gohtml", respond.TplData{
|
||||
"BasePath": s.cfg.Web.BasePath,
|
||||
"Version": internal.Version,
|
||||
"Year": time.Now().Year(),
|
||||
"Version": internal.Version,
|
||||
"Year": time.Now().Year(),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) rapiDocHandler(version ApiVersion) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
s.tpl.HTML(w, http.StatusOK, "rapidoc.gohtml", respond.TplData{
|
||||
"RapiDocSource": s.cfg.Web.BasePath + "/js/rapidoc-min.js",
|
||||
"BasePath": s.cfg.Web.BasePath,
|
||||
"ApiSpecUrl": fmt.Sprintf("%s/doc/%s_swagger.yaml", s.cfg.Web.BasePath, version),
|
||||
"RapiDocSource": "/js/rapidoc-min.js",
|
||||
"ApiSpecUrl": fmt.Sprintf("/doc/%s_swagger.yaml", version),
|
||||
"Version": internal.Version,
|
||||
"Year": time.Now().Year(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) updateBasePathInFrontend(useEmbeddedFrontend bool) ([]byte, []byte, string) {
|
||||
if s.cfg.Web.BasePath == "" {
|
||||
return nil, nil, "" // nothing to do
|
||||
}
|
||||
|
||||
var customIndexFile []byte
|
||||
if useEmbeddedFrontend {
|
||||
customIndexFile, _ = fs.ReadFile(fsMust(fs.Sub(frontendStatics, "frontend-dist")), "index.html")
|
||||
} else {
|
||||
customIndexFile, _ = os.ReadFile(filepath.Join(s.cfg.Web.FrontendFilePath, "index.html"))
|
||||
}
|
||||
newIndexStr := strings.ReplaceAll(string(customIndexFile), "src=\"/", "src=\""+s.cfg.Web.BasePath+"/")
|
||||
newIndexStr = strings.ReplaceAll(newIndexStr, "href=\"/", "href=\""+s.cfg.Web.BasePath+"/")
|
||||
|
||||
re := regexp.MustCompile(`/app/assets/(index-.+.css)`)
|
||||
match := re.FindStringSubmatch(newIndexStr)
|
||||
cssFileName := match[1]
|
||||
|
||||
var customCssFile []byte
|
||||
if useEmbeddedFrontend {
|
||||
customCssFile, _ = fs.ReadFile(fsMust(fs.Sub(frontendStatics, "frontend-dist")), "assets/"+cssFileName)
|
||||
} else {
|
||||
customCssFile, _ = os.ReadFile(filepath.Join(s.cfg.Web.FrontendFilePath, "/assets/", cssFileName))
|
||||
}
|
||||
newCssStr := strings.ReplaceAll(string(customCssFile), "/app/assets/", s.cfg.Web.BasePath+"/app/assets/")
|
||||
|
||||
return []byte(newIndexStr), []byte(newCssStr), cssFileName
|
||||
}
|
||||
|
||||
func fsMust(f fs.FS, err error) fs.FS {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
||||
@@ -67,8 +67,7 @@ func (e ConfigEndpoint) RegisterRoutes(g *routegroup.Bundle) {
|
||||
// @Router /config/frontend.js [get]
|
||||
func (e ConfigEndpoint) handleConfigJsGet() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
basePath := e.cfg.Web.BasePath
|
||||
backendUrl := fmt.Sprintf("%s%s/api/v0", e.cfg.Web.ExternalUrl, basePath)
|
||||
backendUrl := fmt.Sprintf("%s/api/v0", e.cfg.Web.ExternalUrl)
|
||||
if request.Header(r, "x-wg-dev") != "" {
|
||||
referer := request.Header(r, "Referer")
|
||||
host := "localhost"
|
||||
@@ -77,13 +76,12 @@ func (e ConfigEndpoint) handleConfigJsGet() http.HandlerFunc {
|
||||
if err == nil {
|
||||
host, port, _ = net.SplitHostPort(parsedReferer.Host)
|
||||
}
|
||||
backendUrl = fmt.Sprintf("http://%s:%s%s/api/v0", host,
|
||||
port, basePath) // override if request comes from frontend started with npm run dev
|
||||
backendUrl = fmt.Sprintf("http://%s:%s/api/v0", host,
|
||||
port) // override if request comes from frontend started with npm run dev
|
||||
}
|
||||
|
||||
e.tpl.Render(w, http.StatusOK, "frontend_config.js.gotpl", "text/javascript", map[string]any{
|
||||
"BackendUrl": backendUrl,
|
||||
"BasePath": basePath,
|
||||
"Version": internal.Version,
|
||||
"SiteTitle": e.cfg.Web.SiteTitle,
|
||||
"SiteCompanyName": e.cfg.Web.SiteCompanyName,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
WGPORTAL_BACKEND_BASE_URL="{{ $.BackendUrl }}";
|
||||
WGPORTAL_BASE_PATH="{{ $.BasePath }}";
|
||||
WGPORTAL_VERSION="{{ $.Version }}";
|
||||
WGPORTAL_SITE_TITLE="{{ $.SiteTitle }}";
|
||||
WGPORTAL_SITE_COMPANY_NAME="{{ $.SiteCompanyName }}";
|
||||
|
||||
@@ -49,11 +49,7 @@ func NewSessionWrapper(cfg *config.Config) *SessionWrapper {
|
||||
sessionManager.Cookie.Secure = strings.HasPrefix(cfg.Web.ExternalUrl, "https")
|
||||
sessionManager.Cookie.HttpOnly = true
|
||||
sessionManager.Cookie.SameSite = http.SameSiteLaxMode
|
||||
if cfg.Web.BasePath != "" {
|
||||
sessionManager.Cookie.Path = cfg.Web.BasePath
|
||||
} else {
|
||||
sessionManager.Cookie.Path = "/"
|
||||
}
|
||||
sessionManager.Cookie.Path = "/"
|
||||
sessionManager.Cookie.Persist = false
|
||||
|
||||
wrappedSessionManager := &SessionWrapper{sessionManager}
|
||||
|
||||
@@ -99,7 +99,7 @@ type Authenticator struct {
|
||||
}
|
||||
|
||||
// NewAuthenticator creates a new Authenticator instance.
|
||||
func NewAuthenticator(cfg *config.Auth, extUrl, basePath string, bus EventBus, users UserManager) (
|
||||
func NewAuthenticator(cfg *config.Auth, extUrl string, bus EventBus, users UserManager) (
|
||||
*Authenticator,
|
||||
error,
|
||||
) {
|
||||
@@ -107,7 +107,7 @@ func NewAuthenticator(cfg *config.Auth, extUrl, basePath string, bus EventBus, u
|
||||
cfg: cfg,
|
||||
bus: bus,
|
||||
users: users,
|
||||
callbackUrlPrefix: fmt.Sprintf("%s%s/api/v0", extUrl, basePath),
|
||||
callbackUrlPrefix: fmt.Sprintf("%s/api/v0", extUrl),
|
||||
oauthAuthenticators: make(map[string]AuthenticatorOauth, len(cfg.OpenIDConnect)+len(cfg.OAuth)),
|
||||
ldapAuthenticators: make(map[string]AuthenticatorLdap, len(cfg.Ldap)),
|
||||
}
|
||||
|
||||
@@ -149,7 +149,6 @@ func defaultConfig() *Config {
|
||||
RequestLogging: getEnvBool("WG_PORTAL_WEB_REQUEST_LOGGING", false),
|
||||
ExposeHostInfo: getEnvBool("WG_PORTAL_WEB_EXPOSE_HOST_INFO", false),
|
||||
ExternalUrl: getEnvStr("WG_PORTAL_WEB_EXTERNAL_URL", "http://localhost:8888"),
|
||||
BasePath: getEnvStr("WG_PORTAL_WEB_BASE_PATH", ""),
|
||||
ListeningAddress: getEnvStr("WG_PORTAL_WEB_LISTENING_ADDRESS", ":8888"),
|
||||
SessionIdentifier: getEnvStr("WG_PORTAL_WEB_SESSION_IDENTIFIER", "wgPortalSession"),
|
||||
SessionSecret: getEnvStr("WG_PORTAL_WEB_SESSION_SECRET", "very_secret"),
|
||||
|
||||
@@ -11,10 +11,6 @@ type WebConfig struct {
|
||||
// ExternalUrl is the URL where a client can access WireGuard Portal.
|
||||
// This is used for the callback URL of the OAuth providers.
|
||||
ExternalUrl string `yaml:"external_url"`
|
||||
// BasePath is an optional URL path prefix under which the whole web UI and API are served.
|
||||
// Example: "/wg" will make the UI available at /wg/app and the API at /wg/api.
|
||||
// Empty string means no prefix (served from root path).
|
||||
BasePath string `yaml:"base_path"`
|
||||
// ListeningAddress is the address and port for the web server.
|
||||
ListeningAddress string `yaml:"listening_address"`
|
||||
// SessionIdentifier is the session identifier for the web frontend.
|
||||
@@ -39,12 +35,4 @@ type WebConfig struct {
|
||||
|
||||
func (c *WebConfig) Sanitize() {
|
||||
c.ExternalUrl = strings.TrimRight(c.ExternalUrl, "/")
|
||||
|
||||
// normalize BasePath: allow empty, otherwise ensure leading slash, no trailing slash
|
||||
p := strings.TrimSpace(c.BasePath)
|
||||
p = strings.TrimRight(p, "/")
|
||||
if p != "" && !strings.HasPrefix(p, "/") {
|
||||
p = "/" + p
|
||||
}
|
||||
c.BasePath = p
|
||||
}
|
||||
|
||||
@@ -81,6 +81,7 @@ nav:
|
||||
- Reverse Proxy (HTTPS): documentation/getting-started/reverse-proxy.md
|
||||
- Configuration:
|
||||
- Overview: documentation/configuration/overview.md
|
||||
- Mail templates: documentation/configuration/mail-templates.md
|
||||
- Examples: documentation/configuration/examples.md
|
||||
- Usage:
|
||||
- General: documentation/usage/general.md
|
||||
|
||||
@@ -7,13 +7,11 @@ After=network.target
|
||||
Type=simple
|
||||
User=root
|
||||
Group=root
|
||||
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW
|
||||
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
|
||||
WorkingDirectory=/opt/wg-portal
|
||||
Environment=WG_PORTAL_CONFIG=/opt/wg-portal/config.yml
|
||||
ExecStart=/opt/wg-portal/wg-portal-amd64
|
||||
|
||||
[Install]
|
||||
|
||||
Reference in New Issue
Block a user