feat: add some colors to cli commands

This commit is contained in:
Stavros
2026-07-02 17:49:56 +03:00
parent 54992d4f6f
commit 92172af095
8 changed files with 504 additions and 367 deletions
+25 -4
View File
@@ -1,11 +1,12 @@
package main package main
import ( import (
"encoding/json"
"fmt" "fmt"
"strings"
"github.com/tinyauthapp/paerser/cli" "github.com/tinyauthapp/paerser/cli"
"github.com/tinyauthapp/tinyauth/internal/model" "github.com/tinyauthapp/tinyauth/internal/model"
"gopkg.in/yaml.v3"
) )
func configCmd(tconfig *model.Config, loaders []cli.ResourceLoader) *cli.Command { func configCmd(tconfig *model.Config, loaders []cli.ResourceLoader) *cli.Command {
@@ -15,11 +16,31 @@ func configCmd(tconfig *model.Config, loaders []cli.ResourceLoader) *cli.Command
Configuration: tconfig, Configuration: tconfig,
Resources: loaders, Resources: loaders,
Run: func(_ []string) error { Run: func(_ []string) error {
jsonBytes, err := json.MarshalIndent(tconfig, "", " ") buf := strings.Builder{}
fmt.Fprint(&buf, "Your current configuration in YAML is:\n\n")
yout, err := yaml.Marshal(&tconfig)
if err != nil { if err != nil {
return fmt.Errorf("failed to marshal configuration: %w", err) return fmt.Errorf("failed to marshal yaml: %w", err)
} }
fmt.Println(string(jsonBytes))
for l := range strings.SplitSeq(string(yout), "\n") {
if l == "" {
continue
}
lp := strings.SplitN(l, ":", 2)
buf.WriteString(redStyle.Render(lp[0]))
buf.WriteString(grayStyle.Render(":"))
if len(lp) == 2 {
buf.WriteString("")
buf.WriteString(greenStyle.Render(lp[1]))
}
buf.WriteString("\n")
}
fmt.Println(buf.String())
return nil return nil
}, },
} }
+59 -17
View File
@@ -7,8 +7,10 @@ import (
"strings" "strings"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/tinyauthapp/tinyauth/internal/utils"
"github.com/tinyauthapp/paerser/cli" "github.com/tinyauthapp/paerser/cli"
"github.com/tinyauthapp/tinyauth/internal/model"
"github.com/tinyauthapp/tinyauth/internal/utils"
"gopkg.in/yaml.v3"
) )
func createOidcClientCmd() *cli.Command { func createOidcClientCmd() *cli.Command {
@@ -38,33 +40,73 @@ func createOidcClientCmd() *cli.Command {
uclientName := strings.ToUpper(clientName) uclientName := strings.ToUpper(clientName)
lclientName := strings.ToLower(clientName) lclientName := strings.ToLower(clientName)
builder := strings.Builder{} buf := strings.Builder{}
// header // header
fmt.Fprintf(&builder, "Created credentials for client %s\n\n", clientName) fmt.Fprintf(&buf, "Created '%s' OIDC client.\n\n", clientName)
// credentials // credentials
fmt.Fprintf(&builder, "Client Name: %s\n", clientName) fmt.Fprintf(&buf, "Credentials:\n\n")
fmt.Fprintf(&builder, "Client ID: %s\n", clientId) fmt.Fprintf(&buf, "Client Name: %s\n", clientName)
fmt.Fprintf(&builder, "Client Secret: %s\n\n", clientSecret) fmt.Fprintf(&buf, "Client ID: %s\n", clientId)
fmt.Fprintf(&buf, "Client Secret: %s\n\n", clientSecret)
// env variables // end variables
fmt.Fprint(&builder, "Environment variables:\n\n") fmt.Fprintf(&buf, "Environment variables:\n\n")
fmt.Fprintf(&builder, "TINYAUTH_OIDC_CLIENTS_%s_CLIENTID=%s\n", uclientName, clientId) renderToBuf(&buf, map[string]string{
fmt.Fprintf(&builder, "TINYAUTH_OIDC_CLIENTS_%s_CLIENTSECRET=%s\n", uclientName, clientSecret) fmt.Sprintf("TINYAUTH_OIDC_CLIENTS_%s_CLIENTID", uclientName): clientId,
fmt.Fprintf(&builder, "TINYAUTH_OIDC_CLIENTS_%s_NAME=%s\n\n", uclientName, utils.Capitalize(lclientName)) fmt.Sprintf("TINYAUTH_OIDC_CLIENTS_%s_CLIENTSECRET", uclientName): clientSecret,
fmt.Sprintf("TINYAUTH_OIDC_CLIENTS_%s_NAME", uclientName): utils.Capitalize(lclientName),
}, "=")
fmt.Fprintf(&buf, "\n")
// cli flags // cli flags
fmt.Fprint(&builder, "CLI flags:\n\n") fmt.Fprintf(&buf, "CLI flags:\n\n")
fmt.Fprintf(&builder, "--oidc.clients.%s.clientid=%s\n", lclientName, clientId) renderToBuf(&buf, map[string]string{
fmt.Fprintf(&builder, "--oidc.clients.%s.clientsecret=%s\n", lclientName, clientSecret) fmt.Sprintf("--oidc-clients-%s-clientid", lclientName): clientId,
fmt.Fprintf(&builder, "--oidc.clients.%s.name=%s\n\n", lclientName, utils.Capitalize(lclientName)) fmt.Sprintf("--oidc-clients-%s-clientsecret", lclientName): clientSecret,
fmt.Sprintf("--oidc-clients-%s-name", lclientName): utils.Capitalize(lclientName),
}, "=")
fmt.Fprintf(&buf, "\n")
// yaml config
fmt.Fprintf(&buf, "YAML config:\n\n")
yout, err := yaml.Marshal(&model.OIDCConfig{
Clients: map[string]model.OIDCClientConfig{
lclientName: {
ClientID: clientId,
ClientSecret: clientSecret,
Name: utils.Capitalize(lclientName),
},
},
})
if err != nil {
return fmt.Errorf("failed to marshal yaml: %w", err)
}
for l := range strings.SplitSeq(string(yout), "\n") {
if l == "" {
continue
}
lp := strings.SplitN(l, ":", 2)
buf.WriteString(redStyle.Render(lp[0]))
buf.WriteString(grayStyle.Render(":"))
if len(lp) == 2 {
buf.WriteString("")
buf.WriteString(greenStyle.Render(lp[1]))
}
buf.WriteString("\n")
}
buf.WriteString("\n")
// footer // footer
fmt.Fprintln(&builder, "You can use either option to configure your OIDC client. Make sure to save these credentials as there is no way to regenerate them.") fmt.Fprintln(&buf, "You can use any of the above options to configure your OIDC client. Make sure to save these credentials as there is no way to regenerate them.")
// print // print
out := builder.String() out := buf.String()
fmt.Print(out) fmt.Print(out)
return nil return nil
}, },
+96 -54
View File
@@ -3,11 +3,11 @@ package main
import ( import (
"errors" "errors"
"fmt" "fmt"
"os"
"strings" "strings"
"charm.land/huh/v2" "charm.land/huh/v2"
"github.com/tinyauthapp/paerser/cli" "github.com/tinyauthapp/paerser/cli"
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
@@ -34,62 +34,104 @@ func createUserCmd() *cli.Command {
&cli.FlagLoader{}, &cli.FlagLoader{},
} }
return &cli.Command{ cmd := &cli.Command{
Name: "create", Name: "create",
Description: "Create a user", Description: "Create a user",
Configuration: tCfg, Configuration: tCfg,
Resources: loaders, Resources: loaders,
Run: func(_ []string) error {
log := logger.NewLogger().WithSimpleConfig()
log.Init()
if tCfg.Interactive {
form := huh.NewForm(
huh.NewGroup(
huh.NewInput().Title("Username").Value(&tCfg.Username).Validate((func(s string) error {
if s == "" {
return errors.New("username cannot be empty")
}
return nil
})),
huh.NewInput().Title("Password").Value(&tCfg.Password).Validate((func(s string) error {
if s == "" {
return errors.New("password cannot be empty")
}
return nil
})),
huh.NewSelect[bool]().Title("Format the output for Docker?").Options(huh.NewOption("Yes", true), huh.NewOption("No", false)).Value(&tCfg.Docker),
),
)
theme := new(themeBase)
err := form.WithTheme(theme).Run()
if err != nil {
return fmt.Errorf("failed to run interactive prompt: %w", err)
}
}
if tCfg.Username == "" || tCfg.Password == "" {
return errors.New("username and password cannot be empty")
}
log.App.Info().Str("username", tCfg.Username).Msg("Creating user")
passwd, err := bcrypt.GenerateFromPassword([]byte(tCfg.Password), bcrypt.DefaultCost)
if err != nil {
return fmt.Errorf("failed to hash password: %w", err)
}
// If docker format is enabled, escape the dollar sign
passwdStr := string(passwd)
if tCfg.Docker {
passwdStr = strings.ReplaceAll(passwdStr, "$", "$$")
}
log.App.Info().Str("user", fmt.Sprintf("%s:%s", tCfg.Username, passwdStr)).Msg("User created")
return nil
},
} }
cmd.Run = func(_ []string) error {
if tCfg.Interactive {
form := huh.NewForm(
huh.NewGroup(
huh.NewInput().Title("Username").Value(&tCfg.Username).Validate(func(s string) error {
if s == "" {
return errors.New("username cannot be empty")
}
if strings.Contains(s, ":") {
return errors.New("username cannot contain ':'")
}
return nil
}),
huh.NewInput().Title("Password").Value(&tCfg.Password).Validate(func(s string) error {
if s == "" {
return errors.New("password cannot be empty")
}
return nil
}),
huh.NewSelect[bool]().Title("Format the output for Docker?").Options(huh.NewOption("Yes", true), huh.NewOption("No", false)).Value(&tCfg.Docker),
),
)
theme := new(themeBase)
err := form.WithTheme(theme).Run()
if err != nil {
return fmt.Errorf("failed to run interactive prompt: %w", err)
}
}
if tCfg.Username == "" || tCfg.Password == "" {
cmd.PrintHelp(os.Stdout)
return errors.New("username and password cannot be empty")
}
if strings.Contains(tCfg.Username, ":") {
return errors.New("username cannot contain ':'")
}
passwd, err := bcrypt.GenerateFromPassword([]byte(tCfg.Password), bcrypt.DefaultCost)
if err != nil {
return fmt.Errorf("failed to hash password: %w", err)
}
// Only the docker compose output needs $ escaped, the raw hash is correct everywhere else
passwdStr := string(passwd)
dockerStr := passwdStr
if tCfg.Docker {
dockerStr = strings.ReplaceAll(passwdStr, "$", "$$")
}
user := fmt.Sprintf("%s:%s", tCfg.Username, dockerStr)
escapedUser := strings.ReplaceAll(user, "$", "$$")
escapedUser = `"` + escapedUser + `"`
buf := strings.Builder{}
// header
fmt.Fprintf(&buf, "Created user '%s'.\n\n", tCfg.Username)
// environment variable
fmt.Fprint(&buf, "Environment variable:\n\n")
renderToBuf(&buf, map[string]string{
"TINYAUTH_AUTH_USERS": escapedUser,
}, "=")
// cli flags
fmt.Fprint(&buf, "\nCLI flags:\n\n")
renderToBuf(&buf, map[string]string{
"--auth.users": escapedUser,
}, "=")
// yaml config
fmt.Fprint(&buf, "\nYAML config:\n\n")
buf.WriteString(redStyle.Render("auth"))
buf.WriteString(grayStyle.Render(":"))
buf.WriteString("\n")
buf.WriteString(redStyle.Render(" users"))
buf.WriteString(grayStyle.Render(":"))
buf.WriteString(" ")
buf.WriteString(greenStyle.Render(user))
buf.WriteString("\n\n")
// footer
fmt.Fprint(&buf, "Use your config option of choice to add the user to Tinyauth and then restart.")
fmt.Println(buf.String())
return nil
}
return cmd
} }
+87 -77
View File
@@ -7,7 +7,6 @@ import (
"strings" "strings"
"github.com/tinyauthapp/tinyauth/internal/utils" "github.com/tinyauthapp/tinyauth/internal/utils"
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
"charm.land/huh/v2" "charm.land/huh/v2"
"github.com/mdp/qrterminal/v3" "github.com/mdp/qrterminal/v3"
@@ -34,85 +33,96 @@ func generateTotpCmd() *cli.Command {
&cli.FlagLoader{}, &cli.FlagLoader{},
} }
return &cli.Command{ cmd := &cli.Command{
Name: "generate", Name: "generate",
Description: "Generate a TOTP secret", Description: "Generate a TOTP secret",
Configuration: tCfg, Configuration: tCfg,
Resources: loaders, Resources: loaders,
Run: func(_ []string) error {
log := logger.NewLogger().WithSimpleConfig()
log.Init()
if tCfg.Interactive {
form := huh.NewForm(
huh.NewGroup(
huh.NewInput().Title("Current user (username:hash)").Value(&tCfg.User).Validate((func(s string) error {
if s == "" {
return errors.New("user cannot be empty")
}
return nil
})),
),
)
theme := new(themeBase)
err := form.WithTheme(theme).Run()
if err != nil {
return fmt.Errorf("failed to run interactive prompt: %w", err)
}
}
user, err := utils.ParseUser(tCfg.User)
if err != nil {
return fmt.Errorf("failed to parse user: %w", err)
}
docker := false
if strings.Contains(tCfg.User, "$$") {
docker = true
}
if user.TOTPSecret != "" {
return fmt.Errorf("user already has a TOTP secret")
}
key, err := totp.Generate(totp.GenerateOpts{
Issuer: "Tinyauth",
AccountName: user.Username,
})
if err != nil {
return fmt.Errorf("failed to generate TOTP secret: %w", err)
}
secret := key.Secret()
log.App.Info().Str("secret", secret).Msg("Generated TOTP secret")
log.App.Info().Msg("Generated QR code")
config := qrterminal.Config{
Level: qrterminal.L,
Writer: os.Stdout,
BlackChar: qrterminal.BLACK,
WhiteChar: qrterminal.WHITE,
QuietZone: 2,
}
qrterminal.GenerateWithConfig(key.URL(), config)
user.TOTPSecret = secret
// If using docker escape re-escape it
if docker {
user.Password = strings.ReplaceAll(user.Password, "$", "$$")
}
log.App.Info().Str("user", fmt.Sprintf("%s:%s:%s", user.Username, user.Password, user.TOTPSecret)).Msg("Add the totp secret to your authenticator app then use the verify command to ensure everything is working correctly.")
return nil
},
} }
cmd.Run = func(_ []string) error {
if tCfg.Interactive {
form := huh.NewForm(
huh.NewGroup(
huh.NewInput().Title("Current user (username:hash)").Value(&tCfg.User).Validate((func(s string) error {
if s == "" {
return errors.New("user cannot be empty")
}
return nil
})),
),
)
theme := new(themeBase)
err := form.WithTheme(theme).Run()
if err != nil {
return fmt.Errorf("failed to run interactive prompt: %w", err)
}
}
if tCfg.User == "" {
cmd.PrintHelp(os.Stdout)
return fmt.Errorf("user is required")
}
user, err := utils.ParseUser(tCfg.User)
if err != nil {
return fmt.Errorf("failed to parse user: %w", err)
}
docker := false
if strings.Contains(tCfg.User, "$$") {
docker = true
}
if user.TOTPSecret != "" {
return fmt.Errorf("user already has a TOTP secret")
}
key, err := totp.Generate(totp.GenerateOpts{
Issuer: "Tinyauth",
AccountName: user.Username,
})
if err != nil {
return fmt.Errorf("failed to generate TOTP secret: %w", err)
}
secret := key.Secret()
fmt.Printf("Scan the following QR code with your authenticator app (e.g., Google Authenticator, 2fauth, Microsoft Authenticator):\n\n")
config := qrterminal.Config{
Level: qrterminal.L,
Writer: os.Stdout,
BlackChar: qrterminal.BLACK,
WhiteChar: qrterminal.WHITE,
QuietZone: 2,
}
qrterminal.GenerateWithConfig(key.URL(), config)
user.TOTPSecret = secret
// If using docker escape re-escape it
if docker {
user.Password = strings.ReplaceAll(user.Password, "$", "$$")
}
userStr := fmt.Sprintf("%s:%s:%s", user.Username, user.Password, user.TOTPSecret)
fmt.Print("\nOr add the following TOTP secret to your authenticator app: ")
fmt.Print(greenStyle.Render(secret))
fmt.Print("\n\n")
fmt.Printf("Finally, add your user '%s' back to your configuration: ", user.Username)
fmt.Print(greenStyle.Render(userStr))
fmt.Print("\n")
return nil
}
return cmd
} }
+19 -1
View File
@@ -6,6 +6,7 @@ import (
"strings" "strings"
"charm.land/huh/v2" "charm.land/huh/v2"
"charm.land/lipgloss/v2"
"github.com/tinyauthapp/tinyauth/internal/bootstrap" "github.com/tinyauthapp/tinyauth/internal/bootstrap"
"github.com/tinyauthapp/tinyauth/internal/model" "github.com/tinyauthapp/tinyauth/internal/model"
"github.com/tinyauthapp/tinyauth/internal/utils/loaders" "github.com/tinyauthapp/tinyauth/internal/utils/loaders"
@@ -129,7 +130,7 @@ func main() {
fmt.Println("Command not found. Use 'tinyauth help' to see available commands.") fmt.Println("Command not found. Use 'tinyauth help' to see available commands.")
return return
} }
if strings.Contains(err.Error(), "command is not runnable") { if strings.Contains(err.Error(), "is not runnable") {
return return
} }
fatalf(err, "Failed to execute command") fatalf(err, "Failed to execute command")
@@ -154,7 +155,24 @@ func (t *themeBase) Theme(isDark bool) *huh.Styles {
return huh.ThemeBase(isDark) return huh.ThemeBase(isDark)
} }
var (
redStyle = lipgloss.NewStyle().Foreground(lipgloss.Red)
greenStyle = lipgloss.NewStyle().Foreground(lipgloss.Green)
grayStyle = lipgloss.NewStyle().Foreground(lipgloss.Lighten(lipgloss.Black, 0.8))
yellowStyle = lipgloss.NewStyle().Foreground(lipgloss.Yellow)
blueStyle = lipgloss.NewStyle().Foreground(lipgloss.Blue)
)
func fatalf(err error, msg string) { func fatalf(err error, msg string) {
fmt.Printf("%s: %v\n", msg, err) fmt.Printf("%s: %v\n", msg, err)
os.Exit(1) os.Exit(1)
} }
func renderToBuf(buf *strings.Builder, kv map[string]string, sep string) {
for k, v := range kv {
buf.WriteString(redStyle.Render(k))
buf.WriteString(grayStyle.Render(sep))
buf.WriteString(greenStyle.Render(v))
buf.WriteString("\n")
}
}
+77 -73
View File
@@ -3,9 +3,9 @@ package main
import ( import (
"errors" "errors"
"fmt" "fmt"
"os"
"github.com/tinyauthapp/tinyauth/internal/utils" "github.com/tinyauthapp/tinyauth/internal/utils"
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
"charm.land/huh/v2" "charm.land/huh/v2"
"github.com/pquerna/otp/totp" "github.com/pquerna/otp/totp"
@@ -38,81 +38,85 @@ func verifyUserCmd() *cli.Command {
&cli.FlagLoader{}, &cli.FlagLoader{},
} }
return &cli.Command{ cmd := &cli.Command{
Name: "verify", Name: "verify",
Description: "Verify a user is set up correctly", Description: "Verify a user is set up correctly",
Configuration: tCfg, Configuration: tCfg,
Resources: loaders, Resources: loaders,
Run: func(_ []string) error {
log := logger.NewLogger().WithSimpleConfig()
log.Init()
if tCfg.Interactive {
form := huh.NewForm(
huh.NewGroup(
huh.NewInput().Title("User (username:hash:totp)").Value(&tCfg.User).Validate((func(s string) error {
if s == "" {
return errors.New("user cannot be empty")
}
return nil
})),
huh.NewInput().Title("Username").Value(&tCfg.Username).Validate((func(s string) error {
if s == "" {
return errors.New("username cannot be empty")
}
return nil
})),
huh.NewInput().Title("Password").Value(&tCfg.Password).Validate((func(s string) error {
if s == "" {
return errors.New("password cannot be empty")
}
return nil
})),
huh.NewInput().Title("TOTP Code (optional)").Value(&tCfg.Totp),
),
)
theme := new(themeBase)
err := form.WithTheme(theme).Run()
if err != nil {
return fmt.Errorf("failed to run interactive prompt: %w", err)
}
}
user, err := utils.ParseUser(tCfg.User)
if err != nil {
return fmt.Errorf("failed to parse user: %w", err)
}
if user.Username != tCfg.Username {
return fmt.Errorf("username is incorrect")
}
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(tCfg.Password))
if err != nil {
return fmt.Errorf("password is incorrect: %w", err)
}
if user.TOTPSecret == "" {
if tCfg.Totp != "" {
log.App.Warn().Msg("User does not have TOTP secret")
}
log.App.Info().Msg("User verified")
return nil
}
ok := totp.Validate(tCfg.Totp, user.TOTPSecret)
if !ok {
return fmt.Errorf("TOTP code incorrect")
}
log.App.Info().Msg("User verified")
return nil
},
} }
cmd.Run = func(_ []string) error {
if tCfg.Interactive {
form := huh.NewForm(
huh.NewGroup(
huh.NewInput().Title("User (username:hash:totp)").Value(&tCfg.User).Validate((func(s string) error {
if s == "" {
return errors.New("user cannot be empty")
}
return nil
})),
huh.NewInput().Title("Username").Value(&tCfg.Username).Validate((func(s string) error {
if s == "" {
return errors.New("username cannot be empty")
}
return nil
})),
huh.NewInput().Title("Password").Value(&tCfg.Password).Validate((func(s string) error {
if s == "" {
return errors.New("password cannot be empty")
}
return nil
})),
huh.NewInput().Title("TOTP Code (optional)").Value(&tCfg.Totp),
),
)
theme := new(themeBase)
err := form.WithTheme(theme).Run()
if err != nil {
return fmt.Errorf("failed to run interactive prompt: %w", err)
}
}
if tCfg.User == "" || tCfg.Username == "" || tCfg.Password == "" {
cmd.PrintHelp(os.Stdout)
return fmt.Errorf("user, username, and password are required")
}
user, err := utils.ParseUser(tCfg.User)
if err != nil {
return fmt.Errorf("failed to parse user: %w", err)
}
if user.Username != tCfg.Username {
return fmt.Errorf("username is incorrect")
}
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(tCfg.Password))
if err != nil {
return fmt.Errorf("password is incorrect: %w", err)
}
if user.TOTPSecret == "" {
if tCfg.Totp != "" {
fmt.Println(yellowStyle.Render("⚠") + " TOTP code provided but user does not have TOTP enabled")
}
fmt.Println(greenStyle.Render("✓") + " User verified")
return nil
}
ok := totp.Validate(tCfg.Totp, user.TOTPSecret)
if !ok {
return fmt.Errorf("TOTP code incorrect")
}
fmt.Println(greenStyle.Render("✓") + " User verified")
return nil
}
return cmd
} }
+3 -3
View File
@@ -14,9 +14,9 @@ func versionCmd() *cli.Command {
Configuration: nil, Configuration: nil,
Resources: nil, Resources: nil,
Run: func(_ []string) error { Run: func(_ []string) error {
fmt.Printf("Version: %s\n", model.Version) fmt.Printf("Version: %s\n", blueStyle.Render(model.Version))
fmt.Printf("Commit Hash: %s\n", model.CommitHash) fmt.Printf("Commit Hash: %s\n", blueStyle.Render(model.CommitHash))
fmt.Printf("Build Timestamp: %s\n", model.BuildTimestamp) fmt.Printf("Build Timestamp: %s\n", blueStyle.Render(model.BuildTimestamp))
return nil return nil
}, },
} }
+138 -138
View File
@@ -100,238 +100,238 @@ func NewDefaultConfiguration(runtimeEnv RuntimeEnv) *Config {
} }
type Config struct { type Config struct {
AppURL string `description:"The base URL where the app is hosted." yaml:"appUrl"` AppURL string `description:"The base URL where the app is hosted." yaml:"appUrl,omitempty"`
Database DatabaseConfig `description:"Database configuration." yaml:"database"` Database DatabaseConfig `description:"Database configuration." yaml:"database,omitempty"`
Analytics AnalyticsConfig `description:"Analytics configuration." yaml:"analytics"` Analytics AnalyticsConfig `description:"Analytics configuration." yaml:"analytics,omitempty"`
Resources ResourcesConfig `description:"Resources configuration." yaml:"resources"` Resources ResourcesConfig `description:"Resources configuration." yaml:"resources,omitempty"`
Server ServerConfig `description:"Server configuration." yaml:"server"` Server ServerConfig `description:"Server configuration." yaml:"server,omitempty"`
Auth AuthConfig `description:"Authentication configuration." yaml:"auth"` Auth AuthConfig `description:"Authentication configuration." yaml:"auth,omitempty"`
Apps map[string]App `description:"Application ACLs configuration." yaml:"apps"` Apps map[string]App `description:"Application ACLs configuration." yaml:"apps,omitempty"`
OAuth OAuthConfig `description:"OAuth configuration." yaml:"oauth"` OAuth OAuthConfig `description:"OAuth configuration." yaml:"oauth,omitempty"`
OIDC OIDCConfig `description:"OIDC configuration." yaml:"oidc"` OIDC OIDCConfig `description:"OIDC configuration." yaml:"oidc,omitempty"`
UI UIConfig `description:"UI customization." yaml:"ui"` UI UIConfig `description:"UI customization." yaml:"ui,omitempty"`
LDAP LDAPConfig `description:"LDAP configuration." yaml:"ldap"` LDAP LDAPConfig `description:"LDAP configuration." yaml:"ldap,omitempty"`
// Experimental ExperimentalConfig `description:"Experimental features, use with caution." yaml:"experimental"` // Experimental ExperimentalConfig `description:"Experimental features, use with caution." yaml:"experimental,omitempty"`
LabelProvider string `description:"Label provider to use for ACLs (auto, docker, kubernetes or none to disable). auto detects the environment." yaml:"labelProvider"` LabelProvider string `description:"Label provider to use for ACLs (auto, docker, kubernetes or none to disable). auto detects the environment." yaml:"labelProvider,omitempty"`
Log LogConfig `description:"Logging configuration." yaml:"log"` Log LogConfig `description:"Logging configuration." yaml:"log,omitempty"`
Tailscale TailscaleConfig `description:"Tailscale configuration." yaml:"tailscale"` Tailscale TailscaleConfig `description:"Tailscale configuration." yaml:"tailscale,omitempty"`
ConfigFile string `description:"Path to config file." yaml:"-"` ConfigFile string `description:"Path to config file." yaml:"-"`
} }
type DatabaseConfig struct { type DatabaseConfig struct {
Driver string `description:"The database driver to use. Valid values: sqlite, postgres, memory." yaml:"driver"` Driver string `description:"The database driver to use. Valid values: sqlite, postgres, memory." yaml:"driver,omitempty"`
Path string `description:"The path to the SQLite database file, or connection URL when driver is postgres." yaml:"path"` Path string `description:"The path to the SQLite database file, or connection URL when driver is postgres." yaml:"path,omitempty"`
} }
type AnalyticsConfig struct { type AnalyticsConfig struct {
Enabled bool `description:"Enable periodic version information collection." yaml:"enabled"` Enabled bool `description:"Enable periodic version information collection." yaml:"enabled,omitempty"`
} }
type ResourcesConfig struct { type ResourcesConfig struct {
Enabled bool `description:"Enable the resources server." yaml:"enabled"` Enabled bool `description:"Enable the resources server." yaml:"enabled,omitempty"`
Path string `description:"The directory where resources are stored." yaml:"path"` Path string `description:"The directory where resources are stored." yaml:"path,omitempty"`
} }
type ServerConfig struct { type ServerConfig struct {
Port int `description:"The port on which the server listens." yaml:"port"` Port int `description:"The port on which the server listens." yaml:"port,omitempty"`
Address string `description:"The address on which the server listens." yaml:"address"` Address string `description:"The address on which the server listens." yaml:"address,omitempty"`
SocketPath string `description:"The path to the Unix socket." yaml:"socketPath"` SocketPath string `description:"The path to the Unix socket." yaml:"socketPath,omitempty"`
} }
type AuthConfig struct { type AuthConfig struct {
IP IPConfig `description:"IP whitelisting config options." yaml:"ip"` IP IPConfig `description:"IP whitelisting config options." yaml:"ip,omitempty"`
Users []string `description:"Comma-separated list of users (username:hashed_password)." yaml:"users"` Users []string `description:"Comma-separated list of users (username:hashed_password)." yaml:"users,omitempty"`
SubdomainsEnabled bool `description:"Enable subdomains support." yaml:"subdomainsEnabled"` SubdomainsEnabled bool `description:"Enable subdomains support." yaml:"subdomainsEnabled,omitempty"`
UserAttributes map[string]UserAttributes `description:"Map of per-user OIDC attributes (username -> attributes)." yaml:"userAttributes"` UserAttributes map[string]UserAttributes `description:"Map of per-user OIDC attributes (username -> attributes)." yaml:"userAttributes,omitempty"`
UsersFile string `description:"Path to the users file." yaml:"usersFile"` UsersFile string `description:"Path to the users file." yaml:"usersFile,omitempty"`
SecureCookie bool `description:"Enable secure cookies." yaml:"secureCookie"` SecureCookie bool `description:"Enable secure cookies." yaml:"secureCookie,omitempty"`
SessionExpiry int `description:"Session expiry time in seconds." yaml:"sessionExpiry"` SessionExpiry int `description:"Session expiry time in seconds." yaml:"sessionExpiry,omitempty"`
SessionMaxLifetime int `description:"Maximum session lifetime in seconds." yaml:"sessionMaxLifetime"` SessionMaxLifetime int `description:"Maximum session lifetime in seconds." yaml:"sessionMaxLifetime,omitempty"`
LoginTimeout int `description:"Login timeout in seconds." yaml:"loginTimeout"` LoginTimeout int `description:"Login timeout in seconds." yaml:"loginTimeout,omitempty"`
LoginMaxRetries int `description:"Maximum login retries." yaml:"loginMaxRetries"` LoginMaxRetries int `description:"Maximum login retries." yaml:"loginMaxRetries,omitempty"`
LockdownEnabled bool `description:"Enable lockdown mode after maximum login retries. Lockdown mode limit is calculated automatically." yaml:"lockdownEnabled"` LockdownEnabled bool `description:"Enable lockdown mode after maximum login retries. Lockdown mode limit is calculated automatically." yaml:"lockdownEnabled,omitempty"`
TrustedProxies []string `description:"Comma-separated list of trusted proxy addresses." yaml:"trustedProxies"` TrustedProxies []string `description:"Comma-separated list of trusted proxy addresses." yaml:"trustedProxies,omitempty"`
ACLs ACLsConfig `description:"ACLs configuration." yaml:"acls"` ACLs ACLsConfig `description:"ACLs configuration." yaml:"acls,omitempty"`
} }
type UserAttributes struct { type UserAttributes struct {
Name string `description:"Full name of the user." yaml:"name"` Name string `description:"Full name of the user." yaml:"name,omitempty"`
GivenName string `description:"Given (first) name of the user." yaml:"givenName"` GivenName string `description:"Given (first) name of the user." yaml:"givenName,omitempty"`
FamilyName string `description:"Family (last) name of the user." yaml:"familyName"` FamilyName string `description:"Family (last) name of the user." yaml:"familyName,omitempty"`
MiddleName string `description:"Middle name of the user." yaml:"middleName"` MiddleName string `description:"Middle name of the user." yaml:"middleName,omitempty"`
Nickname string `description:"Nickname of the user." yaml:"nickname"` Nickname string `description:"Nickname of the user." yaml:"nickname,omitempty"`
Profile string `description:"URL of the user's profile page." yaml:"profile"` Profile string `description:"URL of the user's profile page." yaml:"profile,omitempty"`
Picture string `description:"URL of the user's profile picture." yaml:"picture"` Picture string `description:"URL of the user's profile picture." yaml:"picture,omitempty"`
Website string `description:"URL of the user's website." yaml:"website"` Website string `description:"URL of the user's website." yaml:"website,omitempty"`
Email string `description:"Email address of the user." yaml:"email"` Email string `description:"Email address of the user." yaml:"email,omitempty"`
Gender string `description:"Gender of the user." yaml:"gender"` Gender string `description:"Gender of the user." yaml:"gender,omitempty"`
Birthdate string `description:"Birthdate of the user (YYYY-MM-DD)." yaml:"birthdate"` Birthdate string `description:"Birthdate of the user (YYYY-MM-DD)." yaml:"birthdate,omitempty"`
Zoneinfo string `description:"Time zone of the user (e.g. Europe/Athens)." yaml:"zoneinfo"` Zoneinfo string `description:"Time zone of the user (e.g. Europe/Athens)." yaml:"zoneinfo,omitempty"`
Locale string `description:"Locale of the user (e.g. en-US)." yaml:"locale"` Locale string `description:"Locale of the user (e.g. en-US)." yaml:"locale,omitempty"`
PhoneNumber string `description:"Phone number of the user." yaml:"phoneNumber"` PhoneNumber string `description:"Phone number of the user." yaml:"phoneNumber,omitempty"`
Address AddressClaim `description:"Address of the user." yaml:"address"` Address AddressClaim `description:"Address of the user." yaml:"address,omitempty"`
} }
type AddressClaim struct { type AddressClaim struct {
Formatted string `description:"Full mailing address, formatted for display." yaml:"formatted" json:"formatted,omitempty"` Formatted string `description:"Full mailing address, formatted for display." yaml:"formatted,omitempty" json:"formatted,omitempty"`
StreetAddress string `description:"Street address." yaml:"streetAddress" json:"street_address,omitempty"` StreetAddress string `description:"Street address." yaml:"streetAddress,omitempty" json:"street_address,omitempty"`
Locality string `description:"City or locality." yaml:"locality" json:"locality,omitempty"` Locality string `description:"City or locality." yaml:"locality,omitempty" json:"locality,omitempty"`
Region string `description:"State, province, or region." yaml:"region" json:"region,omitempty"` Region string `description:"State, province, or region." yaml:"region,omitempty" json:"region,omitempty"`
PostalCode string `description:"Zip or postal code." yaml:"postalCode" json:"postal_code,omitempty"` PostalCode string `description:"Zip or postal code." yaml:"postalCode,omitempty" json:"postal_code,omitempty"`
Country string `description:"Country." yaml:"country" json:"country,omitempty"` Country string `description:"Country." yaml:"country,omitempty" json:"country,omitempty"`
} }
type IPConfig struct { type IPConfig struct {
Allow []string `description:"List of allowed IPs or CIDR ranges." yaml:"allow"` Allow []string `description:"List of allowed IPs or CIDR ranges." yaml:"allow,omitempty"`
Block []string `description:"List of blocked IPs or CIDR ranges." yaml:"block"` Block []string `description:"List of blocked IPs or CIDR ranges." yaml:"block,omitempty"`
Bypass []string `description:"List of IPs or CIDR ranges that bypass authentication entirely." yaml:"bypass"` Bypass []string `description:"List of IPs or CIDR ranges that bypass authentication entirely." yaml:"bypass,omitempty"`
} }
type OAuthConfig struct { type OAuthConfig struct {
Whitelist []string `description:"Comma-separated list of allowed OAuth domains." yaml:"whitelist"` Whitelist []string `description:"Comma-separated list of allowed OAuth domains." yaml:"whitelist,omitempty"`
WhitelistFile string `description:"Path to the OAuth whitelist file." yaml:"whitelistFile"` WhitelistFile string `description:"Path to the OAuth whitelist file." yaml:"whitelistFile,omitempty"`
AutoRedirect string `description:"The OAuth provider to use for automatic redirection." yaml:"autoRedirect"` AutoRedirect string `description:"The OAuth provider to use for automatic redirection." yaml:"autoRedirect,omitempty"`
Providers map[string]OAuthServiceConfig `description:"OAuth providers configuration." yaml:"providers"` Providers map[string]OAuthServiceConfig `description:"OAuth providers configuration." yaml:"providers,omitempty"`
} }
type OIDCConfig struct { type OIDCConfig struct {
PrivateKeyPath string `description:"Path to the private key file, including file name." yaml:"privateKeyPath"` PrivateKeyPath string `description:"Path to the private key file, including file name." yaml:"privateKeyPath,omitempty"`
PublicKeyPath string `description:"Path to the public key file, including file name." yaml:"publicKeyPath"` PublicKeyPath string `description:"Path to the public key file, including file name." yaml:"publicKeyPath,omitempty"`
Clients map[string]OIDCClientConfig `description:"OIDC clients configuration." yaml:"clients"` Clients map[string]OIDCClientConfig `description:"OIDC clients configuration." yaml:"clients,omitempty"`
} }
type UIConfig struct { type UIConfig struct {
Title string `description:"The title of the UI." yaml:"title"` Title string `description:"The title of the UI." yaml:"title,omitempty"`
ForgotPasswordMessage string `description:"Message displayed on the forgot password page." yaml:"forgotPasswordMessage"` ForgotPasswordMessage string `description:"Message displayed on the forgot password page." yaml:"forgotPasswordMessage,omitempty"`
BackgroundImage string `description:"Path to the background image." yaml:"backgroundImage"` BackgroundImage string `description:"Path to the background image." yaml:"backgroundImage,omitempty"`
WarningsEnabled bool `description:"Enable UI warnings." yaml:"warningsEnabled"` WarningsEnabled bool `description:"Enable UI warnings." yaml:"warningsEnabled,omitempty"`
} }
type LDAPConfig struct { type LDAPConfig struct {
Address string `description:"LDAP server address." yaml:"address"` Address string `description:"LDAP server address." yaml:"address,omitempty"`
BindDN string `description:"Bind DN for LDAP authentication." yaml:"bindDn"` BindDN string `description:"Bind DN for LDAP authentication." yaml:"bindDn,omitempty"`
BindPassword string `description:"Bind password for LDAP authentication." yaml:"bindPassword"` BindPassword string `description:"Bind password for LDAP authentication." yaml:"bindPassword,omitempty"`
BindPasswordFile string `description:"Path to the Bind password." yaml:"bindPasswordFile"` BindPasswordFile string `description:"Path to the Bind password." yaml:"bindPasswordFile,omitempty"`
BaseDN string `description:"Base DN for LDAP searches." yaml:"baseDn"` BaseDN string `description:"Base DN for LDAP searches." yaml:"baseDn,omitempty"`
Insecure bool `description:"Allow insecure LDAP connections." yaml:"insecure"` Insecure bool `description:"Allow insecure LDAP connections." yaml:"insecure,omitempty"`
SearchFilter string `description:"LDAP search filter." yaml:"searchFilter"` SearchFilter string `description:"LDAP search filter." yaml:"searchFilter,omitempty"`
AuthCert string `description:"Certificate for mTLS authentication." yaml:"authCert"` AuthCert string `description:"Certificate for mTLS authentication." yaml:"authCert,omitempty"`
AuthKey string `description:"Certificate key for mTLS authentication." yaml:"authKey"` AuthKey string `description:"Certificate key for mTLS authentication." yaml:"authKey,omitempty"`
GroupCacheTTL int `description:"Cache duration for LDAP group membership in seconds." yaml:"groupCacheTTL"` GroupCacheTTL int `description:"Cache duration for LDAP group membership in seconds." yaml:"groupCacheTTL,omitempty"`
} }
type LogConfig struct { type LogConfig struct {
Level string `description:"Log level (trace, debug, info, warn, error)." yaml:"level"` Level string `description:"Log level (trace, debug, info, warn, error)." yaml:"level,omitempty"`
Json bool `description:"Enable JSON formatted logs." yaml:"json"` Json bool `description:"Enable JSON formatted logs." yaml:"json,omitempty"`
Streams LogStreams `description:"Configuration for specific log streams." yaml:"streams"` Streams LogStreams `description:"Configuration for specific log streams." yaml:"streams,omitempty"`
} }
type LogStreams struct { type LogStreams struct {
HTTP LogStreamConfig `description:"HTTP request logging." yaml:"http"` HTTP LogStreamConfig `description:"HTTP request logging." yaml:"http,omitempty"`
App LogStreamConfig `description:"Application logging." yaml:"app"` App LogStreamConfig `description:"Application logging." yaml:"app,omitempty"`
Audit LogStreamConfig `description:"Audit logging." yaml:"audit"` Audit LogStreamConfig `description:"Audit logging." yaml:"audit,omitempty"`
} }
type LogStreamConfig struct { type LogStreamConfig struct {
Enabled bool `description:"Enable this log stream." yaml:"enabled"` Enabled bool `description:"Enable this log stream." yaml:"enabled,omitempty"`
Level string `description:"Log level for this stream. Use global if empty." yaml:"level"` Level string `description:"Log level for this stream. Use global if empty." yaml:"level,omitempty"`
} }
// no experimental features // no experimental features
type ExperimentalConfig struct{} type ExperimentalConfig struct{}
type TailscaleConfig struct { type TailscaleConfig struct {
Enabled bool `description:"Enable Tailscale integration." yaml:"enabled"` Enabled bool `description:"Enable Tailscale integration." yaml:"enabled,omitempty"`
Dir string `description:"Tailscale state directory." yaml:"dir"` Dir string `description:"Tailscale state directory." yaml:"dir,omitempty"`
Hostname string `description:"Tailscale hostname." yaml:"hostname"` Hostname string `description:"Tailscale hostname." yaml:"hostname,omitempty"`
AuthKey string `description:"Tailscale auth key." yaml:"authKey"` AuthKey string `description:"Tailscale auth key." yaml:"authKey,omitempty"`
Ephemeral bool `description:"Use ephemeral Tailscale node." yaml:"ephemeral"` Ephemeral bool `description:"Use ephemeral Tailscale node." yaml:"ephemeral,omitempty"`
Funnel bool `description:"Enable Tailscale Funnel." yaml:"funnel"` Funnel bool `description:"Enable Tailscale Funnel." yaml:"funnel,omitempty"`
Listen bool `description:"Listen on the Tailscale address instead of standard address." yaml:"listen"` Listen bool `description:"Listen on the Tailscale address instead of standard address." yaml:"listen,omitempty"`
} }
// OAuth/OIDC config // OAuth/OIDC config
type OAuthServiceConfig struct { type OAuthServiceConfig struct {
ClientID string `description:"OAuth client ID." yaml:"clientId"` ClientID string `description:"OAuth client ID." yaml:"clientId,omitempty"`
ClientSecret string `description:"OAuth client secret." yaml:"clientSecret"` ClientSecret string `description:"OAuth client secret." yaml:"clientSecret,omitempty"`
ClientSecretFile string `description:"Path to the file containing the OAuth client secret." yaml:"clientSecretFile"` ClientSecretFile string `description:"Path to the file containing the OAuth client secret." yaml:"clientSecretFile,omitempty"`
Whitelist []string `description:"Comma-separated list of allowed OAuth domains for this provider." yaml:"whitelist"` Whitelist []string `description:"Comma-separated list of allowed OAuth domains for this provider." yaml:"whitelist,omitempty"`
WhitelistFile string `description:"Path to the OAuth whitelist file for this provider." yaml:"whitelistFile"` WhitelistFile string `description:"Path to the OAuth whitelist file for this provider." yaml:"whitelistFile,omitempty"`
Scopes []string `description:"OAuth scopes." yaml:"scopes"` Scopes []string `description:"OAuth scopes." yaml:"scopes,omitempty"`
RedirectURL string `description:"OAuth redirect URL." yaml:"redirectUrl"` RedirectURL string `description:"OAuth redirect URL." yaml:"redirectUrl,omitempty"`
AuthURL string `description:"OAuth authorization URL." yaml:"authUrl"` AuthURL string `description:"OAuth authorization URL." yaml:"authUrl,omitempty"`
TokenURL string `description:"OAuth token URL." yaml:"tokenUrl"` TokenURL string `description:"OAuth token URL." yaml:"tokenUrl,omitempty"`
UserinfoURL string `description:"OAuth userinfo URL." yaml:"userinfoUrl"` UserinfoURL string `description:"OAuth userinfo URL." yaml:"userinfoUrl,omitempty"`
Insecure bool `description:"Allow insecure OAuth connections." yaml:"insecure"` Insecure bool `description:"Allow insecure OAuth connections." yaml:"insecure,omitempty"`
Name string `description:"Provider name in UI." yaml:"name"` Name string `description:"Provider name in UI." yaml:"name,omitempty"`
} }
type OIDCClientConfig struct { type OIDCClientConfig struct {
ID string `description:"OIDC client ID." yaml:"-"` ID string `description:"OIDC client ID." yaml:"-"`
ClientID string `description:"OIDC client ID." yaml:"clientId"` ClientID string `description:"OIDC client ID." yaml:"clientId,omitempty"`
ClientSecret string `description:"OIDC client secret." yaml:"clientSecret"` ClientSecret string `description:"OIDC client secret." yaml:"clientSecret,omitempty"`
ClientSecretFile string `description:"Path to the file containing the OIDC client secret." yaml:"clientSecretFile"` ClientSecretFile string `description:"Path to the file containing the OIDC client secret." yaml:"clientSecretFile,omitempty"`
TrustedRedirectURIs []string `description:"List of trusted redirect URIs." yaml:"trustedRedirectUris"` TrustedRedirectURIs []string `description:"List of trusted redirect URIs." yaml:"trustedRedirectUris,omitempty"`
Name string `description:"Client name in UI." yaml:"name"` Name string `description:"Client name in UI." yaml:"name,omitempty"`
} }
type ACLsConfig struct { type ACLsConfig struct {
Policy string `description:"ACL policy for allow-by-default or deny-by-default, available options are allow and deny, default is allow." yaml:"policy"` Policy string `description:"ACL policy for allow-by-default or deny-by-default, available options are allow and deny, default is allow." yaml:"policy,omitempty"`
} }
// ACLs // ACLs
type Apps struct { type Apps struct {
Apps map[string]App `description:"App ACLs configuration." yaml:"apps"` Apps map[string]App `description:"App ACLs configuration." yaml:"apps,omitempty"`
} }
type App struct { type App struct {
Config AppConfig `description:"App configuration." yaml:"config"` Config AppConfig `description:"App configuration." yaml:"config,omitempty"`
Users AppUsers `description:"User access configuration." yaml:"users"` Users AppUsers `description:"User access configuration." yaml:"users,omitempty"`
OAuth AppOAuth `description:"OAuth access configuration." yaml:"oauth"` OAuth AppOAuth `description:"OAuth access configuration." yaml:"oauth,omitempty"`
IP AppIP `description:"IP access configuration." yaml:"ip"` IP AppIP `description:"IP access configuration." yaml:"ip,omitempty"`
Response AppResponse `description:"Response customization." yaml:"response"` Response AppResponse `description:"Response customization." yaml:"response,omitempty"`
Path AppPath `description:"Path access configuration." yaml:"path"` Path AppPath `description:"Path access configuration." yaml:"path,omitempty"`
LDAP AppLDAP `description:"LDAP access configuration." yaml:"ldap"` LDAP AppLDAP `description:"LDAP access configuration." yaml:"ldap,omitempty"`
} }
type AppConfig struct { type AppConfig struct {
Domain string `description:"The domain of the app." yaml:"domain"` Domain string `description:"The domain of the app." yaml:"domain,omitempty"`
} }
type AppUsers struct { type AppUsers struct {
Allow string `description:"Comma-separated list of allowed users." yaml:"allow"` Allow string `description:"Comma-separated list of allowed users." yaml:"allow,omitempty"`
Block string `description:"Comma-separated list of blocked users." yaml:"block"` Block string `description:"Comma-separated list of blocked users." yaml:"block,omitempty"`
} }
type AppOAuth struct { type AppOAuth struct {
Whitelist string `description:"Comma-separated list of allowed OAuth groups." yaml:"whitelist"` Whitelist string `description:"Comma-separated list of allowed OAuth groups." yaml:"whitelist,omitempty"`
Groups string `description:"Comma-separated list of required OAuth groups." yaml:"groups"` Groups string `description:"Comma-separated list of required OAuth groups." yaml:"groups,omitempty"`
} }
type AppLDAP struct { type AppLDAP struct {
Groups string `description:"Comma-separated list of required LDAP groups." yaml:"groups"` Groups string `description:"Comma-separated list of required LDAP groups." yaml:"groups,omitempty"`
} }
type AppIP struct { type AppIP struct {
Allow []string `description:"List of allowed IPs or CIDR ranges." yaml:"allow"` Allow []string `description:"List of allowed IPs or CIDR ranges." yaml:"allow,omitempty"`
Block []string `description:"List of blocked IPs or CIDR ranges." yaml:"block"` Block []string `description:"List of blocked IPs or CIDR ranges." yaml:"block,omitempty"`
Bypass []string `description:"List of IPs or CIDR ranges that bypass authentication." yaml:"bypass"` Bypass []string `description:"List of IPs or CIDR ranges that bypass authentication." yaml:"bypass,omitempty"`
} }
type AppResponse struct { type AppResponse struct {
Headers []string `description:"Custom headers to add to the response." yaml:"headers"` Headers []string `description:"Custom headers to add to the response." yaml:"headers,omitempty"`
BasicAuth AppBasicAuth `description:"Basic authentication for the app." yaml:"basicAuth"` BasicAuth AppBasicAuth `description:"Basic authentication for the app." yaml:"basicAuth,omitempty"`
} }
type AppBasicAuth struct { type AppBasicAuth struct {
Username string `description:"Basic auth username." yaml:"username"` Username string `description:"Basic auth username." yaml:"username,omitempty"`
Password string `description:"Basic auth password." yaml:"password"` Password string `description:"Basic auth password." yaml:"password,omitempty"`
PasswordFile string `description:"Path to the file containing the basic auth password." yaml:"passwordFile"` PasswordFile string `description:"Path to the file containing the basic auth password." yaml:"passwordFile,omitempty"`
} }
type AppPath struct { type AppPath struct {
Allow string `description:"Comma-separated list of allowed paths." yaml:"allow"` Allow string `description:"Comma-separated list of allowed paths." yaml:"allow,omitempty"`
Block string `description:"Comma-separated list of blocked paths." yaml:"block"` Block string `description:"Comma-separated list of blocked paths." yaml:"block,omitempty"`
} }