mirror of
https://github.com/h44z/wg-portal.git
synced 2025-12-14 02:26:19 +00:00
Custom templates (#594)
* allow custom mail templates (#533) * allow to override embedded frontend (#533)
This commit is contained in:
@@ -12,6 +12,18 @@ core:
|
|||||||
web:
|
web:
|
||||||
external_url: http://localhost:8888
|
external_url: http://localhost:8888
|
||||||
request_logging: true
|
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:
|
webhook:
|
||||||
url: ""
|
url: ""
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ mail:
|
|||||||
from: Wireguard Portal <noreply@wireguard.local>
|
from: Wireguard Portal <noreply@wireguard.local>
|
||||||
link_only: false
|
link_only: false
|
||||||
allow_peer_email: false
|
allow_peer_email: false
|
||||||
|
templates_path: ""
|
||||||
|
|
||||||
auth:
|
auth:
|
||||||
oidc: []
|
oidc: []
|
||||||
@@ -96,6 +97,7 @@ web:
|
|||||||
expose_host_info: false
|
expose_host_info: false
|
||||||
cert_file: ""
|
cert_file: ""
|
||||||
key_File: ""
|
key_File: ""
|
||||||
|
frontend_filepath: ""
|
||||||
|
|
||||||
webhook:
|
webhook:
|
||||||
url: ""
|
url: ""
|
||||||
@@ -485,6 +487,11 @@ To send emails to all peers that have a valid email-address as user-identifier,
|
|||||||
If false, and the peer has no valid user record linked, emails will not be sent.
|
If false, and the peer has no valid user record linked, emails will not be sent.
|
||||||
If a peer has linked a valid user, the email address is always taken from the user record.
|
If a peer has linked a valid user, the email address is always taken from the user record.
|
||||||
|
|
||||||
|
### `templates_path`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Environment Variable:** `WG_PORTAL_MAIL_TEMPLATES_PATH`
|
||||||
|
- **Description:** Path to the email template files that override embedded templates. Check [usage documentation](../usage/mail-templates.md) for an example.`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Auth
|
## Auth
|
||||||
@@ -841,6 +848,14 @@ Without a valid `external_url`, the login process may fail due to CSRF protectio
|
|||||||
- **Environment Variable:** `WG_PORTAL_WEB_KEY_FILE`
|
- **Environment Variable:** `WG_PORTAL_WEB_KEY_FILE`
|
||||||
- **Description:** (Optional) Path to the TLS certificate key file.
|
- **Description:** (Optional) Path to the TLS certificate key file.
|
||||||
|
|
||||||
|
### `frontend_filepath`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Environment Variable:** `WG_PORTAL_WEB_FRONTEND_FILEPATH`
|
||||||
|
- **Description:** Optional base directory from which the web frontend is served. Check out the [building](../getting-started/sources.md) documentation for more information on how to compile the frontend assets.
|
||||||
|
- If the directory contains at least one file (recursively), these files are served at `/app`, overriding the embedded frontend assets.
|
||||||
|
- If the directory is empty or does not exist on startup, the embedded frontend is copied into this directory automatically and then served.
|
||||||
|
- If left empty, the embedded frontend is served and no files are written to disk.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Webhook
|
## Webhook
|
||||||
|
|||||||
49
docs/documentation/usage/mail-templates.md
Normal file
49
docs/documentation/usage/mail-templates.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
WireGuard Portal sends emails when you share a configuration with a user.
|
||||||
|
By default, the application uses embedded templates. You can fully customize these emails by pointing the Portal
|
||||||
|
to a folder containing your own templates. If the folder is empty on startup, the default embedded templates
|
||||||
|
are written there to get you started.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
To enable custom templates, set the `mail.templates_path` option in the application configuration file
|
||||||
|
or the `WG_PORTAL_MAIL_TEMPLATES_PATH` environment variable to a valid folder path.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
mail:
|
||||||
|
# ... other mail options ...
|
||||||
|
# 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: "/opt/wg-portal/mail-templates"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Template files and names
|
||||||
|
|
||||||
|
The system expects the following template names. Place files with these names in your `templates_path` to override the defaults.
|
||||||
|
You do not need to override all templates, only the ones you want to customize should be present.
|
||||||
|
|
||||||
|
- Text templates (`.gotpl`):
|
||||||
|
- `mail_with_link.gotpl`
|
||||||
|
- `mail_with_attachment.gotpl`
|
||||||
|
- HTML templates (`.gohtml`):
|
||||||
|
- `mail_with_link.gohtml`
|
||||||
|
- `mail_with_attachment.gohtml`
|
||||||
|
|
||||||
|
Both [text](https://pkg.go.dev/text/template) and [HTML templates](https://pkg.go.dev/html/template) are standard Go
|
||||||
|
templates and receive the following data fields, depending on the email type:
|
||||||
|
|
||||||
|
- Common fields:
|
||||||
|
- `PortalUrl` (string) - external URL of the Portal
|
||||||
|
- `PortalName` (string) - site title/company name
|
||||||
|
- `User` (*domain.User) - the recipient user (may be partially populated when sending to a peer email)
|
||||||
|
- Link email (`mail_with_link.*`):
|
||||||
|
- `Link` (string) - the download link
|
||||||
|
- Attachment email (`mail_with_attachment.*`):
|
||||||
|
- `ConfigFileName` (string) - filename of the attached WireGuard config
|
||||||
|
- `QrcodePngName` (string) - CID content-id of the embedded QR code image
|
||||||
|
|
||||||
|
Tip: You can inspect the embedded templates in the repository under [`internal/app/mail/tpl_files/`](https://github.com/h44z/wg-portal/tree/master/internal/app/mail/tpl_files) for reference.
|
||||||
|
When the directory at `templates_path` is empty, these files are copied to your folder so you can edit them in place.
|
||||||
@@ -4,10 +4,12 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-pkgz/routegroup"
|
"github.com/go-pkgz/routegroup"
|
||||||
@@ -155,6 +157,37 @@ func (s *Server) setupFrontendRoutes() {
|
|||||||
respond.Redirect(w, r, http.StatusMovedPermanently, "/app/favicon.ico")
|
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.
|
||||||
|
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)
|
||||||
|
} else {
|
||||||
|
ok := true
|
||||||
|
hasFiles, err := dirHasFiles(s.cfg.Web.FrontendFilePath)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("failed to check frontend base directory", "path", s.cfg.Web.FrontendFilePath, "error", err)
|
||||||
|
ok = false
|
||||||
|
}
|
||||||
|
if !hasFiles && ok {
|
||||||
|
embeddedFS := fsMust(fs.Sub(frontendStatics, "frontend-dist"))
|
||||||
|
if err := copyEmbedDirToDisk(embeddedFS, s.cfg.Web.FrontendFilePath); err != nil {
|
||||||
|
slog.Error("failed to populate frontend base directory from embedded assets",
|
||||||
|
"path", s.cfg.Web.FrontendFilePath, "error", err)
|
||||||
|
ok = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
// serve files from FS
|
||||||
|
slog.Debug("serving frontend files from custom path", "path", s.cfg.Web.FrontendFilePath)
|
||||||
|
s.server.HandleFiles("/app", http.Dir(s.cfg.Web.FrontendFilePath))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: serve embedded frontend files
|
||||||
s.server.HandleFiles("/app", http.FS(fsMust(fs.Sub(frontendStatics, "frontend-dist"))))
|
s.server.HandleFiles("/app", http.FS(fsMust(fs.Sub(frontendStatics, "frontend-dist"))))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,3 +215,67 @@ func fsMust(f fs.FS, err error) fs.FS {
|
|||||||
}
|
}
|
||||||
return f
|
return f
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// dirHasFiles returns true if the directory contains at least one file (non-directory).
|
||||||
|
func dirHasFiles(dir string) (bool, error) {
|
||||||
|
d, err := os.Open(dir)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
defer d.Close()
|
||||||
|
|
||||||
|
// Read a few entries; if any entry exists, consider it having files/dirs.
|
||||||
|
// We want to know if there is at least one file; if only subdirs exist, still treat as content.
|
||||||
|
entries, err := d.Readdir(-1)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
for _, e := range entries {
|
||||||
|
if e.IsDir() {
|
||||||
|
// check recursively
|
||||||
|
has, err := dirHasFiles(filepath.Join(dir, e.Name()))
|
||||||
|
if err == nil && has {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// regular file
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// copyEmbedDirToDisk copies the contents of srcFS into dstDir on disk.
|
||||||
|
func copyEmbedDirToDisk(srcFS fs.FS, dstDir string) error {
|
||||||
|
return fs.WalkDir(srcFS, ".", func(path string, d fs.DirEntry, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
target := filepath.Join(dstDir, path)
|
||||||
|
if d.IsDir() {
|
||||||
|
return os.MkdirAll(target, 0755)
|
||||||
|
}
|
||||||
|
// ensure parent dir exists
|
||||||
|
if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// open source file
|
||||||
|
f, err := srcFS.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
out, err := os.Create(target)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := io.Copy(out, f); err != nil {
|
||||||
|
_ = out.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return out.Close()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ func NewMailManager(
|
|||||||
users UserDatabaseRepo,
|
users UserDatabaseRepo,
|
||||||
wg WireguardDatabaseRepo,
|
wg WireguardDatabaseRepo,
|
||||||
) (*Manager, error) {
|
) (*Manager, error) {
|
||||||
tplHandler, err := newTemplateHandler(cfg.Web.ExternalUrl, cfg.Web.SiteTitle)
|
tplHandler, err := newTemplateHandler(cfg.Web.ExternalUrl, cfg.Web.SiteTitle, cfg.Mail.TemplatesPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to initialize template handler: %w", err)
|
return nil, fmt.Errorf("failed to initialize template handler: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
htmlTemplate "html/template"
|
htmlTemplate "html/template"
|
||||||
"io"
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
"github.com/h44z/wg-portal/internal/domain"
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
@@ -22,15 +26,50 @@ type TemplateHandler struct {
|
|||||||
textTemplates *template.Template
|
textTemplates *template.Template
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTemplateHandler(portalUrl, portalName string) (*TemplateHandler, error) {
|
func newTemplateHandler(portalUrl, portalName string, basePath string) (*TemplateHandler, error) {
|
||||||
|
// Always parse embedded defaults first
|
||||||
htmlTemplateCache, err := htmlTemplate.New("Html").ParseFS(TemplateFiles, "tpl_files/*.gohtml")
|
htmlTemplateCache, err := htmlTemplate.New("Html").ParseFS(TemplateFiles, "tpl_files/*.gohtml")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to parse html template files: %w", err)
|
return nil, fmt.Errorf("failed to parse embedded html template files: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
txtTemplateCache, err := template.New("Txt").ParseFS(TemplateFiles, "tpl_files/*.gotpl")
|
txtTemplateCache, err := template.New("Txt").ParseFS(TemplateFiles, "tpl_files/*.gotpl")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to parse text template files: %w", err)
|
return nil, fmt.Errorf("failed to parse embedded text template files: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a basePath is provided, ensure existence, populate if empty, then parse to override
|
||||||
|
if basePath != "" {
|
||||||
|
if err := os.MkdirAll(basePath, 0755); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create templates base directory %s: %w", basePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hasTemplates, err := dirHasTemplates(basePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to inspect templates directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no templates present, copy embedded defaults to directory
|
||||||
|
if !hasTemplates {
|
||||||
|
if err := copyEmbeddedTemplates(basePath); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to populate templates directory: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse files from basePath to override embedded ones.
|
||||||
|
// Only parse when matches exist to allow partial overrides without errors.
|
||||||
|
if matches, _ := filepath.Glob(filepath.Join(basePath, "*.gohtml")); len(matches) > 0 {
|
||||||
|
slog.Debug("parsing html email templates from base path", "base-path", basePath, "files", matches)
|
||||||
|
if htmlTemplateCache, err = htmlTemplateCache.ParseFiles(matches...); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse html templates from base path: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if matches, _ := filepath.Glob(filepath.Join(basePath, "*.gotpl")); len(matches) > 0 {
|
||||||
|
slog.Debug("parsing text email templates from base path", "base-path", basePath, "files", matches)
|
||||||
|
if txtTemplateCache, err = txtTemplateCache.ParseFiles(matches...); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse text templates from base path: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handler := &TemplateHandler{
|
handler := &TemplateHandler{
|
||||||
@@ -43,24 +82,71 @@ func newTemplateHandler(portalUrl, portalName string) (*TemplateHandler, error)
|
|||||||
return handler, nil
|
return handler, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// dirHasTemplates checks whether directory contains any .gohtml or .gotpl files.
|
||||||
|
func dirHasTemplates(basePath string) (bool, error) {
|
||||||
|
entries, err := os.ReadDir(basePath)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
for _, e := range entries {
|
||||||
|
if e.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ext := filepath.Ext(e.Name())
|
||||||
|
if ext == ".gohtml" || ext == ".gotpl" {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// copyEmbeddedTemplates writes embedded templates into basePath.
|
||||||
|
func copyEmbeddedTemplates(basePath string) error {
|
||||||
|
list, err := fs.ReadDir(TemplateFiles, "tpl_files")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, entry := range list {
|
||||||
|
if entry.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := entry.Name()
|
||||||
|
// Only copy known template extensions
|
||||||
|
if ext := filepath.Ext(name); ext != ".gohtml" && ext != ".gotpl" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
data, err := TemplateFiles.ReadFile(filepath.Join("tpl_files", name))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
out := filepath.Join(basePath, name)
|
||||||
|
if err := os.WriteFile(out, data, 0644); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetConfigMail returns the text and html template for the mail with a link.
|
// GetConfigMail returns the text and html template for the mail with a link.
|
||||||
func (c TemplateHandler) GetConfigMail(user *domain.User, link string) (io.Reader, io.Reader, error) {
|
func (c TemplateHandler) GetConfigMail(user *domain.User, link string) (io.Reader, io.Reader, error) {
|
||||||
var tplBuff bytes.Buffer
|
var tplBuff bytes.Buffer
|
||||||
var htmlTplBuff bytes.Buffer
|
var htmlTplBuff bytes.Buffer
|
||||||
|
|
||||||
err := c.textTemplates.ExecuteTemplate(&tplBuff, "mail_with_link.gotpl", map[string]any{
|
err := c.textTemplates.ExecuteTemplate(&tplBuff, "mail_with_link.gotpl", map[string]any{
|
||||||
"User": user,
|
"User": user,
|
||||||
"Link": link,
|
"Link": link,
|
||||||
"PortalUrl": c.portalUrl,
|
"PortalUrl": c.portalUrl,
|
||||||
|
"PortalName": c.portalName,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("failed to execute template mail_with_link.gotpl: %w", err)
|
return nil, nil, fmt.Errorf("failed to execute template mail_with_link.gotpl: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = c.htmlTemplates.ExecuteTemplate(&htmlTplBuff, "mail_with_link.gohtml", map[string]any{
|
err = c.htmlTemplates.ExecuteTemplate(&htmlTplBuff, "mail_with_link.gohtml", map[string]any{
|
||||||
"User": user,
|
"User": user,
|
||||||
"Link": link,
|
"Link": link,
|
||||||
"PortalUrl": c.portalUrl,
|
"PortalUrl": c.portalUrl,
|
||||||
|
"PortalName": c.portalName,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("failed to execute template mail_with_link.gohtml: %w", err)
|
return nil, nil, fmt.Errorf("failed to execute template mail_with_link.gohtml: %w", err)
|
||||||
|
|||||||
@@ -157,6 +157,7 @@ func defaultConfig() *Config {
|
|||||||
SiteCompanyName: getEnvStr("WG_PORTAL_WEB_SITE_COMPANY_NAME", "WireGuard Portal"),
|
SiteCompanyName: getEnvStr("WG_PORTAL_WEB_SITE_COMPANY_NAME", "WireGuard Portal"),
|
||||||
CertFile: getEnvStr("WG_PORTAL_WEB_CERT_FILE", ""),
|
CertFile: getEnvStr("WG_PORTAL_WEB_CERT_FILE", ""),
|
||||||
KeyFile: getEnvStr("WG_PORTAL_WEB_KEY_FILE", ""),
|
KeyFile: getEnvStr("WG_PORTAL_WEB_KEY_FILE", ""),
|
||||||
|
FrontendFilePath: getEnvStr("WG_PORTAL_WEB_FRONTEND_FILEPATH", ""),
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.Advanced.LogLevel = getEnvStr("WG_PORTAL_ADVANCED_LOG_LEVEL", "info")
|
cfg.Advanced.LogLevel = getEnvStr("WG_PORTAL_ADVANCED_LOG_LEVEL", "info")
|
||||||
@@ -195,6 +196,7 @@ func defaultConfig() *Config {
|
|||||||
From: getEnvStr("WG_PORTAL_MAIL_FROM", "Wireguard Portal <noreply@wireguard.local>"),
|
From: getEnvStr("WG_PORTAL_MAIL_FROM", "Wireguard Portal <noreply@wireguard.local>"),
|
||||||
LinkOnly: getEnvBool("WG_PORTAL_MAIL_LINK_ONLY", false),
|
LinkOnly: getEnvBool("WG_PORTAL_MAIL_LINK_ONLY", false),
|
||||||
AllowPeerEmail: getEnvBool("WG_PORTAL_MAIL_ALLOW_PEER_EMAIL", false),
|
AllowPeerEmail: getEnvBool("WG_PORTAL_MAIL_ALLOW_PEER_EMAIL", false),
|
||||||
|
TemplatesPath: getEnvStr("WG_PORTAL_MAIL_TEMPLATES_PATH", ""),
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.Webhook.Url = getEnvStr("WG_PORTAL_WEBHOOK_URL", "") // no webhook by default
|
cfg.Webhook.Url = getEnvStr("WG_PORTAL_WEBHOOK_URL", "") // no webhook by default
|
||||||
|
|||||||
@@ -43,4 +43,8 @@ type MailConfig struct {
|
|||||||
LinkOnly bool `yaml:"link_only"`
|
LinkOnly bool `yaml:"link_only"`
|
||||||
// AllowPeerEmail specifies whether emails should be sent to peers which have no valid user account linked, but an email address is set as "user".
|
// AllowPeerEmail specifies whether emails should be sent to peers which have no valid user account linked, but an email address is set as "user".
|
||||||
AllowPeerEmail bool `yaml:"allow_peer_email"`
|
AllowPeerEmail bool `yaml:"allow_peer_email"`
|
||||||
|
// TemplatesPath is an optional base path on the filesystem that contains email templates (.gotpl and .gohtml).
|
||||||
|
// If the directory exists but is empty, the embedded default templates will be written there on startup.
|
||||||
|
// If templates are present in the directory, they override the embedded defaults.
|
||||||
|
TemplatesPath string `yaml:"templates_path"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ type WebConfig struct {
|
|||||||
CertFile string `yaml:"cert_file"`
|
CertFile string `yaml:"cert_file"`
|
||||||
// KeyFile is the path to the TLS certificate key file.
|
// KeyFile is the path to the TLS certificate key file.
|
||||||
KeyFile string `yaml:"key_file"`
|
KeyFile string `yaml:"key_file"`
|
||||||
|
// FrontendFilePath is an optional path to a folder that contains the frontend files.
|
||||||
|
// If set and the folder contains at least one file, it overrides the embedded frontend.
|
||||||
|
// If set and the folder is empty or does not exist, the embedded frontend will be written into it on startup.
|
||||||
|
FrontendFilePath string `yaml:"frontend_filepath"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *WebConfig) Sanitize() {
|
func (c *WebConfig) Sanitize() {
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ nav:
|
|||||||
- Reverse Proxy (HTTPS): documentation/getting-started/reverse-proxy.md
|
- Reverse Proxy (HTTPS): documentation/getting-started/reverse-proxy.md
|
||||||
- Configuration:
|
- Configuration:
|
||||||
- Overview: documentation/configuration/overview.md
|
- Overview: documentation/configuration/overview.md
|
||||||
|
- Mail templates: documentation/configuration/mail-templates.md
|
||||||
- Examples: documentation/configuration/examples.md
|
- Examples: documentation/configuration/examples.md
|
||||||
- Usage:
|
- Usage:
|
||||||
- General: documentation/usage/general.md
|
- General: documentation/usage/general.md
|
||||||
@@ -88,6 +89,7 @@ nav:
|
|||||||
- LDAP: documentation/usage/ldap.md
|
- LDAP: documentation/usage/ldap.md
|
||||||
- Security: documentation/usage/security.md
|
- Security: documentation/usage/security.md
|
||||||
- Webhooks: documentation/usage/webhooks.md
|
- Webhooks: documentation/usage/webhooks.md
|
||||||
|
- Mail Templates: documentation/usage/mail-templates.md
|
||||||
- REST API: documentation/rest-api/api-doc.md
|
- REST API: documentation/rest-api/api-doc.md
|
||||||
- Upgrade: documentation/upgrade/v1.md
|
- Upgrade: documentation/upgrade/v1.md
|
||||||
- Monitoring: documentation/monitoring/prometheus.md
|
- Monitoring: documentation/monitoring/prometheus.md
|
||||||
|
|||||||
Reference in New Issue
Block a user