diff --git a/.env.example b/.env.example index 091662f..a89fccb 100644 --- a/.env.example +++ b/.env.example @@ -2,220 +2,171 @@ # The base URL where the app is hosted. TINYAUTH_APPURL= - # The directory where resources are stored. TINYAUTH_RESOURCESDIR="./resources" - # The path to the database file. TINYAUTH_DATABASEPATH="./tinyauth.db" - # Disable analytics. TINYAUTH_DISABLEANALYTICS=false - # Disable resources server. TINYAUTH_DISABLERESOURCES=false +# server config + # The port on which the server listens. TINYAUTH_SERVER_PORT=3000 - # The address on which the server listens. TINYAUTH_SERVER_ADDRESS="0.0.0.0" - # The path to the Unix socket. TINYAUTH_SERVER_SOCKETPATH= +# auth config + # List of allowed IPs or CIDR ranges. TINYAUTH_AUTH_IP_ALLOW= - # List of blocked IPs or CIDR ranges. TINYAUTH_AUTH_IP_BLOCK= - # Comma-separated list of users (username:hashed_password). TINYAUTH_AUTH_USERS= - # Path to the users file. TINYAUTH_AUTH_USERSFILE= - # Enable secure cookies. TINYAUTH_AUTH_SECURECOOKIE=false - # Session expiry time in seconds. TINYAUTH_AUTH_SESSIONEXPIRY=86400 - # Maximum session lifetime in seconds. TINYAUTH_AUTH_SESSIONMAXLIFETIME=0 - # Login timeout in seconds. TINYAUTH_AUTH_LOGINTIMEOUT=300 - # Maximum login retries. TINYAUTH_AUTH_LOGINMAXRETRIES=3 - # Comma-separated list of trusted proxy addresses. TINYAUTH_AUTH_TRUSTEDPROXIES= +# apps config + # The domain of the app. TINYAUTH_APPS_name_CONFIG_DOMAIN= - # Comma-separated list of allowed users. TINYAUTH_APPS_name_USERS_ALLOW= - # Comma-separated list of blocked users. TINYAUTH_APPS_name_USERS_BLOCK= - # Comma-separated list of allowed OAuth groups. TINYAUTH_APPS_name_OAUTH_WHITELIST= - # Comma-separated list of required OAuth groups. TINYAUTH_APPS_name_OAUTH_GROUPS= - # List of allowed IPs or CIDR ranges. TINYAUTH_APPS_name_IP_ALLOW= - # List of blocked IPs or CIDR ranges. TINYAUTH_APPS_name_IP_BLOCK= - # List of IPs or CIDR ranges that bypass authentication. TINYAUTH_APPS_name_IP_BYPASS= - # Custom headers to add to the response. TINYAUTH_APPS_name_RESPONSE_HEADERS= - # Basic auth username. TINYAUTH_APPS_name_RESPONSE_BASICAUTH_USERNAME= - # Basic auth password. TINYAUTH_APPS_name_RESPONSE_BASICAUTH_PASSWORD= - # Path to the file containing the basic auth password. TINYAUTH_APPS_name_RESPONSE_BASICAUTH_PASSWORDFILE= - # Comma-separated list of allowed paths. TINYAUTH_APPS_name_PATH_ALLOW= - # Comma-separated list of blocked paths. TINYAUTH_APPS_name_PATH_BLOCK= - # Comma-separated list of required LDAP groups. TINYAUTH_APPS_name_LDAP_GROUPS= +# oauth config + # Comma-separated list of allowed OAuth domains. TINYAUTH_OAUTH_WHITELIST= - # The OAuth provider to use for automatic redirection. TINYAUTH_OAUTH_AUTOREDIRECT= - # OAuth client ID. TINYAUTH_OAUTH_PROVIDERS_name_CLIENTID= - # OAuth client secret. TINYAUTH_OAUTH_PROVIDERS_name_CLIENTSECRET= - # Path to the file containing the OAuth client secret. TINYAUTH_OAUTH_PROVIDERS_name_CLIENTSECRETFILE= - # OAuth scopes. TINYAUTH_OAUTH_PROVIDERS_name_SCOPES= - # OAuth redirect URL. TINYAUTH_OAUTH_PROVIDERS_name_REDIRECTURL= - # OAuth authorization URL. TINYAUTH_OAUTH_PROVIDERS_name_AUTHURL= - # OAuth token URL. TINYAUTH_OAUTH_PROVIDERS_name_TOKENURL= - # OAuth userinfo URL. TINYAUTH_OAUTH_PROVIDERS_name_USERINFOURL= - # Allow insecure OAuth connections. TINYAUTH_OAUTH_PROVIDERS_name_INSECURE=false - # Provider name in UI. TINYAUTH_OAUTH_PROVIDERS_name_NAME= +# oidc config + # Path to the private key file. TINYAUTH_OIDC_PRIVATEKEYPATH="./tinyauth_oidc_key" - # Path to the public key file. TINYAUTH_OIDC_PUBLICKEYPATH="./tinyauth_oidc_key.pub" - # OIDC client ID. TINYAUTH_OIDC_CLIENTS_name_CLIENTID= - # OIDC client secret. TINYAUTH_OIDC_CLIENTS_name_CLIENTSECRET= - # Path to the file containing the OIDC client secret. TINYAUTH_OIDC_CLIENTS_name_CLIENTSECRETFILE= - # List of trusted redirect URIs. TINYAUTH_OIDC_CLIENTS_name_TRUSTEDREDIRECTURIS= - # Client name in UI. TINYAUTH_OIDC_CLIENTS_name_NAME= +# ui config + # The title of the UI. TINYAUTH_UI_TITLE="Tinyauth" - # Message displayed on the forgot password page. TINYAUTH_UI_FORGOTPASSWORDMESSAGE="You can change your password by changing the configuration." - # Path to the background image. TINYAUTH_UI_BACKGROUNDIMAGE="/background.jpg" - # Disable UI warnings. TINYAUTH_UI_DISABLEWARNINGS=false +# ldap config + # LDAP server address. TINYAUTH_LDAP_ADDRESS= - # Bind DN for LDAP authentication. TINYAUTH_LDAP_BINDDN= - # Bind password for LDAP authentication. TINYAUTH_LDAP_BINDPASSWORD= - # Base DN for LDAP searches. TINYAUTH_LDAP_BASEDN= - # Allow insecure LDAP connections. TINYAUTH_LDAP_INSECURE=false - # LDAP search filter. TINYAUTH_LDAP_SEARCHFILTER="(uid=%s)" - # Certificate for mTLS authentication. TINYAUTH_LDAP_AUTHCERT= - # Certificate key for mTLS authentication. TINYAUTH_LDAP_AUTHKEY= - # Cache duration for LDAP group membership in seconds. TINYAUTH_LDAP_GROUPCACHETTL=900 +# log config + # Log level (trace, debug, info, warn, error). TINYAUTH_LOG_LEVEL="info" - # Enable JSON formatted logs. TINYAUTH_LOG_JSON=false - # Enable this log stream. TINYAUTH_LOG_STREAMS_HTTP_ENABLED=true - # Log level for this stream. Use global if empty. TINYAUTH_LOG_STREAMS_HTTP_LEVEL= - # Enable this log stream. TINYAUTH_LOG_STREAMS_APP_ENABLED=true - # Log level for this stream. Use global if empty. TINYAUTH_LOG_STREAMS_APP_LEVEL= - # Enable this log stream. TINYAUTH_LOG_STREAMS_AUDIT_ENABLED=false - # Log level for this stream. Use global if empty. TINYAUTH_LOG_STREAMS_AUDIT_LEVEL= - diff --git a/.gitignore b/.gitignore index 47fa146..1c25087 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,6 @@ __debug_* # traefik data /traefik + +# generated markdown (for docs) +/config.gen.md diff --git a/config.example.yaml b/config.example.yaml deleted file mode 100644 index fecc823..0000000 --- a/config.example.yaml +++ /dev/null @@ -1,102 +0,0 @@ -# Tinyauth Example Configuration - -# The base URL where Tinyauth is accessible -appUrl: "https://auth.example.com" -# Directory for static resources -resourcesDir: "./resources" -# Path to SQLite database file -databasePath: "./tinyauth.db" -# Disable usage analytics -disableAnalytics: false -# Disable static resource serving -disableResources: false -# Disable UI warning messages -disableUIWarnings: false - -# Logging Configuration -log: - # Log level: trace, debug, info, warn, error - level: "info" - json: false - streams: - app: - enabled: true - level: "warn" - http: - enabled: true - level: "debug" - audit: - enabled: false - level: "info" - -# Server Configuration -server: - # Port to listen on - port: 3000 - # Interface to bind to (0.0.0.0 for all interfaces) - address: "0.0.0.0" - # Unix socket path (optional, overrides port/address if set) - socketPath: "" - # Comma-separated list of trusted proxy IPs/CIDRs - trustedProxies: "" - -# Authentication Configuration -auth: - # Format: username:bcrypt_hash (use bcrypt to generate hash) - users: "admin:$2a$10$example_bcrypt_hash_here" - # Path to external users file (optional) - usersFile: "" - # Enable secure cookies (requires HTTPS) - secureCookie: false - # Session expiry in seconds (3600 = 1 hour) - sessionExpiry: 3600 - # Session maximum lifetime in seconds (0 = unlimited) - sessionMaxLifetime: 0 - # Login timeout in seconds (300 = 5 minutes) - loginTimeout: 300 - # Maximum login retries before lockout - loginMaxRetries: 3 - -# OAuth Configuration -oauth: - # Regex pattern for allowed email addresses (e.g., /@example\.com$/) - whitelist: "" - # Provider ID to auto-redirect to (skips login page) - autoRedirect: "" - # OAuth Provider Configuration (replace myprovider with your provider name) - providers: - myprovider: - clientId: "your_client_id_here" - clientSecret: "your_client_secret_here" - authUrl: "https://provider.example.com/oauth/authorize" - tokenUrl: "https://provider.example.com/oauth/token" - userInfoUrl: "https://provider.example.com/oauth/userinfo" - redirectUrl: "https://auth.example.com/api/oauth/callback/myprovider" - scopes: "openid email profile" - name: "My OAuth Provider" - # Allow insecure connections (self-signed certificates) - insecure: false - -# UI Customization -ui: - # Custom title for login page - title: "Tinyauth" - # Message shown on forgot password page - forgotPasswordMessage: "Contact your administrator to reset your password" - # Background image URL for login page - backgroundImage: "" - -# LDAP Configuration (optional) -ldap: - # LDAP server address - address: "ldap://ldap.example.com:389" - # DN for binding to LDAP server - bindDn: "cn=readonly,dc=example,dc=com" - # Password for bind DN - bindPassword: "your_bind_password" - # Base DN for user searches - baseDn: "dc=example,dc=com" - # Search filter (%s will be replaced with username) - searchFilter: "(&(uid=%s)(memberOf=cn=users,ou=groups,dc=example,dc=com))" - # Allow insecure LDAP connections - insecure: false diff --git a/gen/gen.go b/gen/gen.go index 520eec8..f84bc4d 100644 --- a/gen/gen.go +++ b/gen/gen.go @@ -2,10 +2,37 @@ package main import ( "log/slog" + "reflect" ) func main() { slog.Info("generating example env file") - generateExampleEnv() + slog.Info("generating config reference markdown file") + generateMarkdown() +} + +func walkAndBuild[T any](parent reflect.Type, parentValue reflect.Value, + parentPath string, entries *[]T, + buildEntry func(child reflect.StructField, childValue reflect.Value, parentPath string, entries *[]T), + buildMap func(child reflect.StructField, parentPath string, entries *[]T), + buildChildPath func(parentPath string, childName string) string, +) { + for i := 0; i < parent.NumField(); i++ { + field := parent.Field(i) + fieldType := field.Type + fieldValue := parentValue.Field(i) + + switch fieldType.Kind() { + case reflect.Struct: + childPath := buildChildPath(parentPath, field.Name) + walkAndBuild[T](fieldType, fieldValue, childPath, entries, buildEntry, buildMap, buildChildPath) + case reflect.Map: + buildMap(field, parentPath, entries) + case reflect.Bool, reflect.String, reflect.Slice, reflect.Int: + buildEntry(field, fieldValue, parentPath, entries) + default: + slog.Info("unknown type", "type", fieldType.Kind()) + } + } } diff --git a/gen/gen_env.go b/gen/gen_env.go index 8db51c9..a16fdad 100644 --- a/gen/gen_env.go +++ b/gen/gen_env.go @@ -2,7 +2,9 @@ package main import ( "bytes" + "errors" "fmt" + "io/fs" "log/slog" "os" "reflect" @@ -11,7 +13,7 @@ import ( "github.com/steveiliop56/tinyauth/internal/config" ) -type Path struct { +type EnvEntry struct { Name string Description string Value any @@ -19,17 +21,17 @@ type Path struct { func generateExampleEnv() { cfg := config.NewDefaultConfiguration() - paths := make([]Path, 0) + entries := make([]EnvEntry, 0) root := reflect.TypeOf(cfg).Elem() rootValue := reflect.ValueOf(cfg).Elem() rootPath := "TINYAUTH_" - buildPaths(root, rootValue, rootPath, &paths) - compiled := compileEnv(paths) + walkAndBuild(root, rootValue, rootPath, &entries, buildEnvEntry, buildEnvMapEntry, buildEnvChildPath) + compiled := compileEnv(entries) err := os.Remove(".env.example") - if err != nil { + if err != nil && !errors.Is(err, fs.ErrNotExist) { slog.Error("failed to remove example env file", "error", err) os.Exit(1) } @@ -41,96 +43,88 @@ func generateExampleEnv() { } } -func buildPaths(parent reflect.Type, parentValue reflect.Value, parentPath string, paths *[]Path) { - for i := 0; i < parent.NumField(); i++ { - field := parent.Field(i) - fieldType := field.Type - fieldValue := parentValue.Field(i) - switch fieldType.Kind() { - case reflect.Struct: - childPath := parentPath + strings.ToUpper(field.Name) + "_" - buildPaths(fieldType, fieldValue, childPath, paths) - case reflect.Map: - buildMapPaths(field, parentPath, paths) - case reflect.Bool, reflect.String, reflect.Slice, reflect.Int: - buildPath(field, fieldValue, parentPath, paths) - default: - slog.Info("unknown type", "type", fieldType.Kind()) - } - } -} +func buildEnvEntry(child reflect.StructField, childValue reflect.Value, parentPath string, entries *[]EnvEntry) { + desc := child.Tag.Get("description") + tag := child.Tag.Get("yaml") -func buildPath(field reflect.StructField, fieldValue reflect.Value, parent string, paths *[]Path) { - desc := field.Tag.Get("description") - yamlTag := field.Tag.Get("yaml") - - // probably internal logic, should be skipped - if yamlTag == "-" { + if tag == "-" { return } - defaultValue := fieldValue.Interface() + value := childValue.Interface() - path := Path{ - Name: parent + strings.ToUpper(field.Name), + entry := EnvEntry{ + Name: parentPath + strings.ToUpper(child.Name), Description: desc, } - switch fieldValue.Kind() { + switch childValue.Kind() { case reflect.Slice: - sl, ok := defaultValue.([]string) + sl, ok := value.([]string) if !ok { - slog.Error("invalid default value", "value", defaultValue) + slog.Error("invalid default value", "value", value) return } - path.Value = strings.Join(sl, ",") + entry.Value = strings.Join(sl, ",") case reflect.String: - st, ok := defaultValue.(string) + st, ok := value.(string) if !ok { - slog.Error("invalid default value", "value", defaultValue) + slog.Error("invalid default value", "value", value) return } - // good idea to escape strings probably if st != "" { - path.Value = fmt.Sprintf(`"%s"`, st) + entry.Value = fmt.Sprintf(`"%s"`, st) } else { - path.Value = "" + entry.Value = "" } default: - path.Value = defaultValue + entry.Value = value } - *paths = append(*paths, path) + *entries = append(*entries, entry) } -func buildMapPaths(field reflect.StructField, parentPath string, paths *[]Path) { - fieldType := field.Type +func buildEnvMapEntry(child reflect.StructField, parentPath string, entries *[]EnvEntry) { + fieldType := child.Type if fieldType.Key().Kind() != reflect.String { slog.Info("unsupported map key type", "type", fieldType.Key().Kind()) return } - mapPath := parentPath + strings.ToUpper(field.Name) + "_name_" + mapPath := parentPath + strings.ToUpper(child.Name) + "_name_" valueType := fieldType.Elem() if valueType.Kind() == reflect.Struct { zeroValue := reflect.New(valueType).Elem() - buildPaths(valueType, zeroValue, mapPath, paths) + walkAndBuild(valueType, zeroValue, mapPath, entries, buildEnvEntry, buildEnvMapEntry, buildEnvChildPath) } } -func compileEnv(paths []Path) []byte { +func buildEnvChildPath(parent string, child string) string { + return parent + strings.ToUpper(child) + "_" +} + +func compileEnv(entries []EnvEntry) []byte { buffer := bytes.Buffer{} buffer.WriteString("# Tinyauth example configuration\n\n") - for _, path := range paths { + previousSection := "" + + for _, entry := range entries { + if strings.Count(entry.Name, "_") > 1 { + section := strings.Split(strings.TrimPrefix(entry.Name, "TINYAUTH_"), "_")[0] + if section != previousSection { + buffer.WriteString("\n# " + strings.ToLower(section) + " config\n\n") + previousSection = section + } + } buffer.WriteString("# ") - buffer.WriteString(path.Description) + buffer.WriteString(entry.Description) buffer.WriteString("\n") - buffer.WriteString(path.Name) + buffer.WriteString(entry.Name) buffer.WriteString("=") - fmt.Fprintf(&buffer, "%v", path.Value) - buffer.WriteString("\n\n") + fmt.Fprintf(&buffer, "%v", entry.Value) + buffer.WriteString("\n") } return buffer.Bytes() diff --git a/gen/gen_md.go b/gen/gen_md.go new file mode 100644 index 0000000..1e03f64 --- /dev/null +++ b/gen/gen_md.go @@ -0,0 +1,127 @@ +package main + +import ( + "bytes" + "errors" + "fmt" + "io/fs" + "log/slog" + "os" + "reflect" + "strings" + + "github.com/steveiliop56/tinyauth/internal/config" +) + +type MarkdownEntry struct { + Env string + Flag string + Description string + Default any +} + +func generateMarkdown() { + cfg := config.NewDefaultConfiguration() + entries := make([]MarkdownEntry, 0) + + root := reflect.TypeOf(cfg).Elem() + rootValue := reflect.ValueOf(cfg).Elem() + rootPath := "tinyauth." + + walkAndBuild(root, rootValue, rootPath, &entries, buildMdEntry, buildMdMapEntry, buildMdChildPath) + compiled := compileMd(entries) + + err := os.Remove("config.gen.md") + if err != nil && !errors.Is(err, fs.ErrNotExist) { + slog.Error("failed to remove example env file", "error", err) + os.Exit(1) + } + + err = os.WriteFile("config.gen.md", compiled, 0644) + if err != nil { + slog.Error("failed to write example env file", "error", err) + os.Exit(1) + } +} + +func buildMdEntry(child reflect.StructField, childValue reflect.Value, parentPath string, entries *[]MarkdownEntry) { + desc := child.Tag.Get("description") + tag := child.Tag.Get("yaml") + + if tag == "-" { + return + } + + value := childValue.Interface() + + entry := MarkdownEntry{ + Env: strings.ToUpper(strings.ReplaceAll(parentPath, ".", "_")) + strings.ToUpper(child.Name), + Flag: fmt.Sprintf("--%s%s", strings.TrimPrefix(parentPath, "tinyauth."), tag), + Description: desc, + } + + switch childValue.Kind() { + case reflect.Slice: + sl, ok := value.([]string) + if !ok { + slog.Error("invalid default value", "value", value) + return + } + entry.Default = fmt.Sprintf("`%s`", strings.Join(sl, ",")) + default: + entry.Default = fmt.Sprintf("`%v`", value) + } + *entries = append(*entries, entry) +} + +func buildMdMapEntry(child reflect.StructField, parentPath string, entries *[]MarkdownEntry) { + fieldType := child.Type + + if fieldType.Key().Kind() != reflect.String { + slog.Info("unsupported map key type", "type", fieldType.Key().Kind()) + return + } + + tag := child.Tag.Get("yaml") + + if tag == "-" { + return + } + + mapPath := parentPath + tag + ".[name]." + valueType := fieldType.Elem() + + if valueType.Kind() == reflect.Struct { + zeroValue := reflect.New(valueType).Elem() + walkAndBuild(valueType, zeroValue, mapPath, entries, buildMdEntry, buildMdMapEntry, buildMdChildPath) + } +} + +func buildMdChildPath(parent string, child string) string { + return parent + strings.ToLower(child) + "." +} + +func compileMd(entries []MarkdownEntry) []byte { + buffer := bytes.Buffer{} + + buffer.WriteString("# Tinyauth configuration reference\n\n") + buffer.WriteString("| Environment | Flag | Description | Default |\n") + buffer.WriteString("| - | - | - | - |\n") + + previousSection := "" + + for _, entry := range entries { + if strings.Count(entry.Env, "_") > 1 { + section := strings.Split(strings.TrimPrefix(entry.Env, "TINYAUTH_"), "_")[0] + if section != previousSection { + buffer.WriteString("\n## " + strings.ToLower(section) + "\n\n") + buffer.WriteString("| Environment | Flag | Description | Default |\n") + buffer.WriteString("| - | - | - | - |\n") + previousSection = section + } + } + fmt.Fprintf(&buffer, "| `%s` | `%s` | %s | %s |\n", entry.Env, entry.Flag, entry.Description, entry.Default) + } + + return buffer.Bytes() +}