updated to use bun and updated hws modules.

This commit is contained in:
2026-01-11 23:39:10 +11:00
parent 6e03c98ae8
commit 1eedbc5220
33 changed files with 984 additions and 375 deletions

37
internal/config/auth.go Normal file
View File

@@ -0,0 +1,37 @@
package config
import (
"git.haelnorr.com/h/golib/env"
"github.com/pkg/errors"
)
type HWSAUTHConfig struct {
SSL bool // ENV HWSAUTH_SSL: Flag for SSL Mode (default: false)
TrustedHost string // ENV HWSAUTH_TRUSTED_HOST: Full server address to accept as trusted SSL host (required if SSL is true)
SecretKey string // ENV HWSAUTH_SECRET_KEY: Secret key for signing tokens (required)
AccessTokenExpiry int64 // ENV HWSAUTH_ACCESS_TOKEN_EXPIRY: Access token expiry in minutes (default: 5)
RefreshTokenExpiry int64 // ENV HWSAUTH_REFRESH_TOKEN_EXPIRY: Refresh token expiry in minutes (default: 1440)
TokenFreshTime int64 // ENV HWSAUTH_TOKEN_FRESH_TIME: Time for tokens to stay fresh in minutes (default: 5)
}
func setupHWSAuth() (*HWSAUTHConfig, error) {
ssl := env.Bool("HWSAUTH_SSL", false)
trustedHost := env.String("HWS_TRUSTED_HOST", "")
if ssl && trustedHost == "" {
return nil, errors.New("SSL is enabled and no HWS_TRUSTED_HOST set")
}
cfg := &HWSAUTHConfig{
SSL: ssl,
TrustedHost: trustedHost,
SecretKey: env.String("HWSAUTH_SECRET_KEY", ""),
AccessTokenExpiry: env.Int64("HWSAUTH_ACCESS_TOKEN_EXPIRY", 5),
RefreshTokenExpiry: env.Int64("HWSAUTH_REFRESH_TOKEN_EXPIRY", 1440),
TokenFreshTime: env.Int64("HWSAUTH_TOKEN_FRESH_TIME", 5),
}
if cfg.SecretKey == "" {
return nil, errors.New("Envar not set: HWSAUTH_SECRET_KEY")
}
return cfg, nil
}

View File

@@ -1,116 +1,55 @@
package config
import (
"fmt"
"os"
"time"
"git.haelnorr.com/h/golib/env"
"git.haelnorr.com/h/golib/hlog"
"git.haelnorr.com/h/golib/tmdb"
"git.haelnorr.com/h/golib/hws"
"git.haelnorr.com/h/golib/hwsauth"
"github.com/joho/godotenv"
"github.com/pkg/errors"
)
type Config struct {
Host string // Host to listen on
Port string // Port to listen on
TrustedHost string // Domain/Hostname to accept as trusted
SSL bool // Flag for SSL Mode
GZIP bool // Flag for GZIP compression on requests
ReadHeaderTimeout time.Duration // Timeout for reading request headers in seconds
WriteTimeout time.Duration // Timeout for writing requests in seconds
IdleTimeout time.Duration // Timeout for idle connections in seconds
DBName string // Filename of the db - hardcoded and doubles as DB version
DBLockTimeout time.Duration // Timeout for acquiring database lock
SecretKey string // Secret key for signing tokens
AccessTokenExpiry int64 // Access token expiry in minutes
RefreshTokenExpiry int64 // Refresh token expiry in minutes
TokenFreshTime int64 // Time for tokens to stay fresh in minutes
LogLevel hlog.Level // Log level for global logging. Defaults to info
LogOutput string // "file", "console", or "both". Defaults to console
LogDir string // Path to create log files
TMDBToken string // Read access token for TMDB API
TMDBConfig *tmdb.Config // Config data for interfacing with TMDB
DB *DBConfig
HWS *hws.Config
HWSAuth *hwsauth.Config
TMDB *TMDBConfig
HLOG *HLOGConfig
}
// Load the application configuration and get a pointer to the Config object
func GetConfig(args map[string]string) (*Config, error) {
godotenv.Load(".env")
var (
host string
port string
logLevel hlog.Level
logOutput string
valid bool
)
func GetConfig(envfile string) (*Config, error) {
godotenv.Load(envfile)
if args["host"] != "" {
host = args["host"]
} else {
host = env.String("HOST", "127.0.0.1")
}
if args["port"] != "" {
port = args["port"]
} else {
port = env.String("PORT", "3010")
}
if args["loglevel"] != "" {
logLevel = hlog.LogLevel(args["loglevel"])
} else {
logLevel = hlog.LogLevel(env.String("LOG_LEVEL", "info"))
}
if args["logoutput"] != "" {
opts := map[string]string{
"both": "both",
"file": "file",
"console": "console",
}
logOutput, valid = opts[args["logoutput"]]
if !valid {
logOutput = "console"
fmt.Println(
"Log output type was not parsed correctly. Defaulting to console only",
)
}
} else {
logOutput = env.String("LOG_OUTPUT", "console")
}
if logOutput != "both" && logOutput != "console" && logOutput != "file" {
logOutput = "console"
}
tmdbcfg, err := tmdb.GetConfig(os.Getenv("TMDB_API_TOKEN"))
db, err := setupDB()
if err != nil {
return nil, errors.Wrap(err, "tmdb.GetConfig")
return nil, errors.Wrap(err, "setupDB")
}
hws, err := hws.ConfigFromEnv()
if err != nil {
return nil, errors.Wrap(err, "hws.ConfigFromEnv")
}
hwsAuth, err := hwsauth.ConfigFromEnv()
if err != nil {
return nil, errors.Wrap(err, "hwsauth.ConfigFromEnv")
}
tmdb, err := setupTMDB()
if err != nil {
return nil, errors.Wrap(err, "setupTMDB")
}
hlog, err := setupHLOG()
if err != nil {
return nil, errors.Wrap(err, "setupHLOG")
}
config := &Config{
Host: host,
Port: port,
TrustedHost: env.String("TRUSTED_HOST", "127.0.0.1"),
SSL: env.Bool("SSL_MODE", false),
GZIP: env.Bool("GZIP", false),
ReadHeaderTimeout: env.Duration("READ_HEADER_TIMEOUT", 2) * time.Second,
WriteTimeout: env.Duration("WRITE_TIMEOUT", 10) * time.Second,
IdleTimeout: env.Duration("IDLE_TIMEOUT", 120) * time.Second,
DBName: "00001",
DBLockTimeout: env.Duration("DB_LOCK_TIMEOUT", 60),
SecretKey: env.String("SECRET_KEY", ""),
AccessTokenExpiry: env.Int64("ACCESS_TOKEN_EXPIRY", 5),
RefreshTokenExpiry: env.Int64("REFRESH_TOKEN_EXPIRY", 1440), // defaults to 1 day
TokenFreshTime: env.Int64("TOKEN_FRESH_TIME", 5),
LogLevel: logLevel,
LogOutput: logOutput,
LogDir: env.String("LOG_DIR", ""),
TMDBToken: env.String("TMDB_API_TOKEN", ""),
TMDBConfig: tmdbcfg,
}
if config.SecretKey == "" && args["dbver"] != "true" {
return nil, errors.New("Envar not set: SECRET_KEY")
}
if config.TMDBToken == "" && args["dbver"] != "true" {
return nil, errors.New("Envar not set: TMDB_API_TOKEN")
DB: db,
HWS: hws,
HWSAuth: hwsAuth,
TMDB: tmdb,
HLOG: hlog,
}
return config, nil

55
internal/config/db.go Normal file
View File

@@ -0,0 +1,55 @@
package config
import (
"git.haelnorr.com/h/golib/env"
"github.com/pkg/errors"
)
type DBConfig struct {
User string // ENV DB_USER: Database user for authentication (required)
Password string // ENV DB_PASSWORD: Database password for authentication (required)
Host string // ENV DB_HOST: Database host address (required)
Port uint16 // ENV DB_PORT: Database port (default: 5432)
DB string // ENV DB_NAME: Database name to connect to (required)
SSL string // ENV DB_SSL: SSL mode for connection (default: disable)
}
func setupDB() (*DBConfig, error) {
cfg := &DBConfig{
User: env.String("DB_USER", ""),
Password: env.String("DB_PASSWORD", ""),
Host: env.String("DB_HOST", ""),
Port: env.UInt16("DB_PORT", 5432),
DB: env.String("DB_NAME", ""),
SSL: env.String("DB_SSL", "disable"),
}
// Validate SSL mode
validSSLModes := map[string]bool{
"disable": true,
"require": true,
"verify-ca": true,
"verify-full": true,
"allow": true,
"prefer": true,
}
if !validSSLModes[cfg.SSL] {
return nil, errors.Errorf("Invalid DB_SSL value: %s. Must be one of: disable, allow, prefer, require, verify-ca, verify-full", cfg.SSL)
}
// Check required fields
if cfg.User == "" {
return nil, errors.New("Envar not set: DB_USER")
}
if cfg.Password == "" {
return nil, errors.New("Envar not set: DB_PASSWORD")
}
if cfg.Host == "" {
return nil, errors.New("Envar not set: DB_HOST")
}
if cfg.DB == "" {
return nil, errors.New("Envar not set: DB_NAME")
}
return cfg, nil
}

114
internal/config/envdoc.go Normal file
View File

@@ -0,0 +1,114 @@
package config
import (
"reflect"
"regexp"
"strings"
)
// EnvVar represents an environment variable with its documentation
type EnvVar struct {
Name string
Description string
Default string
HasDefault bool
Required bool
}
// extractEnvVars parses a struct's field comments to extract environment variable documentation
func extractEnvVars(structType reflect.Type, fieldIndex int) *EnvVar {
field := structType.Field(fieldIndex)
tag := field.Tag.Get("comment")
if tag == "" {
// Try to get the comment from the struct field's tag or use reflection
// For now, we'll parse it manually from the comment string
return nil
}
comment := tag
if !strings.HasPrefix(comment, "ENV ") {
return nil
}
// Remove "ENV " prefix
comment = strings.TrimPrefix(comment, "ENV ")
// Extract name and description
parts := strings.SplitN(comment, ":", 2)
if len(parts) != 2 {
return nil
}
name := strings.TrimSpace(parts[0])
desc := strings.TrimSpace(parts[1])
// Check for default value in description
defaultRegex := regexp.MustCompile(`\(default:\s*([^)]+)\)`)
matches := defaultRegex.FindStringSubmatch(desc)
envVar := &EnvVar{
Name: name,
Description: desc,
}
if len(matches) > 1 {
envVar.Default = matches[1]
envVar.HasDefault = true
// Remove the default notation from description
envVar.Description = strings.TrimSpace(defaultRegex.ReplaceAllString(desc, ""))
}
return envVar
}
// GetAllEnvVars returns a list of all environment variables used in the config
func GetAllEnvVars() []EnvVar {
var envVars []EnvVar
// Manually define all env vars based on the config structs
// This is more reliable than reflection for extracting comments
// DBConfig
envVars = append(envVars, []EnvVar{
{Name: "DB_USER", Description: "Database user for authentication", HasDefault: false, Required: true},
{Name: "DB_PASSWORD", Description: "Database password for authentication", HasDefault: false, Required: true},
{Name: "DB_HOST", Description: "Database host address", HasDefault: false, Required: true},
{Name: "DB_PORT", Description: "Database port", Default: "5432", HasDefault: true, Required: false},
{Name: "DB_NAME", Description: "Database name to connect to", HasDefault: false, Required: true},
{Name: "DB_SSL", Description: "SSL mode for connection", Default: "disable", HasDefault: true, Required: false},
}...)
// HWSConfig
envVars = append(envVars, []EnvVar{
{Name: "HWS_HOST", Description: "Host to listen on", Default: "127.0.0.1", HasDefault: true, Required: false},
{Name: "HWS_PORT", Description: "Port to listen on", Default: "3000", HasDefault: true, Required: false},
{Name: "HWS_TRUSTED_HOST", Description: "Domain/Hostname to accept as trusted", Default: "same as Host", HasDefault: true, Required: false},
{Name: "HWS_SSL", Description: "Flag for SSL Mode", Default: "false", HasDefault: true, Required: false},
{Name: "HWS_GZIP", Description: "Flag for GZIP compression on requests", Default: "false", HasDefault: true, Required: false},
{Name: "HWS_READ_HEADER_TIMEOUT", Description: "Timeout for reading request headers in seconds", Default: "2", HasDefault: true, Required: false},
{Name: "HWS_WRITE_TIMEOUT", Description: "Timeout for writing requests in seconds", Default: "10", HasDefault: true, Required: false},
{Name: "HWS_IDLE_TIMEOUT", Description: "Timeout for idle connections in seconds", Default: "120", HasDefault: true, Required: false},
}...)
// HWSAUTHConfig
envVars = append(envVars, []EnvVar{
{Name: "HWSAUTH_SECRET_KEY", Description: "Secret key for signing tokens", HasDefault: false, Required: true},
{Name: "HWSAUTH_ACCESS_TOKEN_EXPIRY", Description: "Access token expiry in minutes", Default: "5", HasDefault: true, Required: false},
{Name: "HWSAUTH_REFRESH_TOKEN_EXPIRY", Description: "Refresh token expiry in minutes", Default: "1440", HasDefault: true, Required: false},
{Name: "HWSAUTH_TOKEN_FRESH_TIME", Description: "Time for tokens to stay fresh in minutes", Default: "5", HasDefault: true, Required: false},
}...)
// TMDBConfig
envVars = append(envVars, []EnvVar{
{Name: "TMDB_TOKEN", Description: "API token for TMDB", HasDefault: false, Required: true},
}...)
// HLOGConfig
envVars = append(envVars, []EnvVar{
{Name: "LOG_LEVEL", Description: "Log level for global logging", Default: "info", HasDefault: true, Required: false},
{Name: "LOG_OUTPUT", Description: "Output method for the logger (file, console, or both)", Default: "console", HasDefault: true, Required: false},
{Name: "LOG_DIR", Description: "Path to create log files", HasDefault: false, Required: false},
}...)
return envVars
}

95
internal/config/envgen.go Normal file
View File

@@ -0,0 +1,95 @@
package config
import (
"fmt"
"os"
"strings"
)
// GenerateDotEnv creates a new .env file with all environment variables and their defaults
func GenerateDotEnv(filename string) error {
envVars := GetAllEnvVars()
file, err := os.Create(filename)
if err != nil {
return fmt.Errorf("failed to create file: %w", err)
}
defer file.Close()
// Write header
fmt.Fprintln(file, "# Environment Configuration")
fmt.Fprintln(file, "# Generated by Project Reshoot")
fmt.Fprintln(file, "#")
fmt.Fprintln(file, "# Variables marked as (required) must be set")
fmt.Fprintln(file, "# Variables with defaults can be left commented out to use the default value")
fmt.Fprintln(file)
// Group by prefix
groups := map[string][]EnvVar{
"DB_": {},
"HWS_": {},
"HWSAUTH_": {},
"TMDB_": {},
"LOG_": {},
}
for _, ev := range envVars {
assigned := false
for prefix := range groups {
if strings.HasPrefix(ev.Name, prefix) {
groups[prefix] = append(groups[prefix], ev)
assigned = true
break
}
}
if !assigned {
// Handle ungrouped vars
if _, ok := groups["OTHER"]; !ok {
groups["OTHER"] = []EnvVar{}
}
groups["OTHER"] = append(groups["OTHER"], ev)
}
}
// Print each group
groupOrder := []string{"DB_", "HWS_", "HWSAUTH_", "TMDB_", "LOG_", "OTHER"}
groupTitles := map[string]string{
"DB_": "Database Configuration",
"HWS_": "HTTP Web Server Configuration",
"HWSAUTH_": "Authentication Configuration",
"TMDB_": "TMDB API Configuration",
"LOG_": "Logging Configuration",
"OTHER": "Other Configuration",
}
for _, prefix := range groupOrder {
vars, ok := groups[prefix]
if !ok || len(vars) == 0 {
continue
}
fmt.Fprintf(file, "# %s\n", groupTitles[prefix])
fmt.Fprintln(file, strings.Repeat("#", len(groupTitles[prefix])+2))
for _, ev := range vars {
// Write description as comment
if ev.Required {
fmt.Fprintf(file, "# %s (required)\n", ev.Description)
// Leave required variables uncommented but empty
fmt.Fprintf(file, "%s=\n", ev.Name)
} else if ev.HasDefault {
fmt.Fprintf(file, "# %s\n", ev.Description)
// Comment out variables with defaults
fmt.Fprintf(file, "# %s=%s\n", ev.Name, ev.Default)
} else {
fmt.Fprintf(file, "# %s\n", ev.Description)
// Optional variables without defaults are commented out
fmt.Fprintf(file, "# %s=\n", ev.Name)
}
fmt.Fprintln(file)
}
fmt.Fprintln(file)
}
return nil
}

View File

@@ -0,0 +1,87 @@
package config
import (
"fmt"
"io"
"strings"
)
// PrintEnvVars writes all environment variables and their documentation to the provided writer
func PrintEnvVars(w io.Writer) error {
envVars := GetAllEnvVars()
// Find the longest name for alignment
maxNameLen := 0
for _, ev := range envVars {
if len(ev.Name) > maxNameLen {
maxNameLen = len(ev.Name)
}
}
// Print header
fmt.Fprintln(w, "Environment Variables")
fmt.Fprintln(w, strings.Repeat("=", 80))
fmt.Fprintln(w)
// Group by prefix
groups := map[string][]EnvVar{
"DB_": {},
"HWS_": {},
"HWSAUTH_": {},
"TMDB_": {},
"LOG_": {},
}
for _, ev := range envVars {
assigned := false
for prefix := range groups {
if strings.HasPrefix(ev.Name, prefix) {
groups[prefix] = append(groups[prefix], ev)
assigned = true
break
}
}
if !assigned {
// Handle ungrouped vars
if _, ok := groups["OTHER"]; !ok {
groups["OTHER"] = []EnvVar{}
}
groups["OTHER"] = append(groups["OTHER"], ev)
}
}
// Print each group
groupOrder := []string{"DB_", "HWS_", "HWSAUTH_", "TMDB_", "LOG_", "OTHER"}
groupTitles := map[string]string{
"DB_": "Database Configuration",
"HWS_": "HTTP Web Server Configuration",
"HWSAUTH_": "Authentication Configuration",
"TMDB_": "TMDB API Configuration",
"LOG_": "Logging Configuration",
"OTHER": "Other Configuration",
}
for _, prefix := range groupOrder {
vars, ok := groups[prefix]
if !ok || len(vars) == 0 {
continue
}
fmt.Fprintf(w, "%s\n", groupTitles[prefix])
fmt.Fprintln(w, strings.Repeat("-", len(groupTitles[prefix])))
for _, ev := range vars {
padding := strings.Repeat(" ", maxNameLen-len(ev.Name))
if ev.Required {
fmt.Fprintf(w, " %s%s : %s (required)\n", ev.Name, padding, ev.Description)
} else if ev.HasDefault {
fmt.Fprintf(w, " %s%s : %s (default: %s)\n", ev.Name, padding, ev.Description, ev.Default)
} else {
fmt.Fprintf(w, " %s%s : %s\n", ev.Name, padding, ev.Description)
}
}
fmt.Fprintln(w)
}
return nil
}

View File

@@ -0,0 +1,29 @@
package config
import (
"time"
"git.haelnorr.com/h/golib/env"
)
type HWSConfig struct {
Host string // ENV HWS_HOST: Host to listen on (default: 127.0.0.1)
Port uint64 // ENV HWS_PORT: Port to listen on (default: 3000)
GZIP bool // ENV HWS_GZIP: Flag for GZIP compression on requests (default: false)
ReadHeaderTimeout time.Duration // ENV HWS_READ_HEADER_TIMEOUT: Timeout for reading request headers in seconds (default: 2)
WriteTimeout time.Duration // ENV HWS_WRITE_TIMEOUT: Timeout for writing requests in seconds (default: 10)
IdleTimeout time.Duration // ENV HWS_IDLE_TIMEOUT: Timeout for idle connections in seconds (default: 120)
}
func setupHWS() (*HWSConfig, error) {
cfg := &HWSConfig{
Host: env.String("HWS_HOST", "127.0.0.1"),
Port: env.UInt64("HWS_PORT", 3000),
GZIP: env.Bool("HWS_GZIP", false),
ReadHeaderTimeout: time.Duration(env.Int("HWS_READ_HEADER_TIMEOUT", 2)) * time.Second,
WriteTimeout: time.Duration(env.Int("HWS_WRITE_TIMEOUT", 10)) * time.Second,
IdleTimeout: time.Duration(env.Int("HWS_IDLE_TIMEOUT", 120)) * time.Second,
}
return cfg, nil
}

36
internal/config/logger.go Normal file
View File

@@ -0,0 +1,36 @@
package config
import (
"git.haelnorr.com/h/golib/env"
"git.haelnorr.com/h/golib/hlog"
"github.com/pkg/errors"
)
type HLOGConfig struct {
// ENV LOG_LEVEL: Log level for global logging. (default: info)
LogLevel hlog.Level
// ENV LOG_OUTPUT: Output method for the logger. (default: console)
// Valid options: "file", "console", "both"
LogOutput string
// ENV LOG_DIR: Path to create log files
LogDir string
}
func setupHLOG() (*HLOGConfig, error) {
logLevel, err := hlog.LogLevel(env.String("LOG_LEVEL", "info"))
if err != nil {
return nil, errors.Wrap(err, "hlog.LogLevel")
}
logOutput := env.String("LOG_OUTPUT", "console")
if logOutput != "both" && logOutput != "console" && logOutput != "file" {
return nil, errors.Errorf("Invalid LOG_OUTPUT: %s", logOutput)
}
cfg := &HLOGConfig{
LogLevel: logLevel,
LogOutput: logOutput,
LogDir: env.String("LOG_DIR", ""),
}
return cfg, nil
}

28
internal/config/tmdb.go Normal file
View File

@@ -0,0 +1,28 @@
package config
import (
"git.haelnorr.com/h/golib/env"
"git.haelnorr.com/h/golib/tmdb"
"github.com/pkg/errors"
)
type TMDBConfig struct {
Token string // ENV TMDB_TOKEN: API token for TMDB (required)
Config *tmdb.Config // Config data for interfacing with TMDB
}
func setupTMDB() (*TMDBConfig, error) {
token := env.String("TMDB_TOKEN", "")
if token == "" {
return nil, errors.New("No TMDB API Token provided")
}
tmdbcfg, err := tmdb.GetConfig(token)
if err != nil {
return nil, errors.Wrap(err, "tmdb.GetConfig")
}
cfg := &TMDBConfig{
Token: token,
Config: tmdbcfg,
}
return cfg, nil
}