From 3cadf065990c94cf7ffd286801a0996228b57e30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20P=C3=A9rez?= Date: Wed, 26 Feb 2025 05:09:17 +0100 Subject: [PATCH] updated app.go --- .env.example | 46 ++++-- LICENSE | 2 +- app/app.go | 367 ++++++++++++++++++++++++++++++++++------- esfaker/esfaker.go | 116 ------------- templates/functions.go | 34 ++++ utils/utils.go | 17 -- 6 files changed, 374 insertions(+), 208 deletions(-) delete mode 100644 esfaker/esfaker.go create mode 100644 templates/functions.go diff --git a/.env.example b/.env.example index e7b0ed0..e7973e9 100644 --- a/.env.example +++ b/.env.example @@ -1,16 +1,32 @@ -# pgx, postgresql, mysql -DRIVERNAME=pgx -# enable / disable migrations -MIGRATE= -# as example -DATASOURCE=postgresql://developer:secret@localhost:5432/db?sslmode=disable -# hex string format -ASYMMETRICKEY= -# in minutes -DURATION= -# SMTP for sending emails -SMTP_HOST=localhost -SMTP_PORT=1025 -SMTP_USER=noreply@example.com -SMTP_PASS=123456 +ENV_DIRECTORY= +ENV_MODE= +LOG_LEVEL= + +APP_NAME= +APP_VERSION= + +TIMEZONE= + +PASETO_ASYMMETRIC_KEY= +PASETO_DURATION= + +SMTP_HOST= +SMTP_PORT= +SMTP_USER= +SMTP_PASS= + +DATABASE_DRIVER_NAME= +DATABASE_DATA_SOURCE= +DATABASE_MIGRATE= + +# if you want override the datasource key, you can use this format _OVERRIDE_KEY +# AUTH_DATA_SOURCE=something_data_source +# example i: OVERRIDE_AUTH_DATA_SOURCE will get the value something_data_source +# example ii: +# app := app.New(app.Config{ +# Name: appName, +# Version: version, +# EnvDirectory: envDirectory, +# DatabaseDataSource: "OVERRIDE_AUTH_DATA_SOURCE", +# }) diff --git a/LICENSE b/LICENSE index c51be33..191eb34 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 Pedro Pérez Banda +Copyright (c) 2025 Pedro Pérez Banda Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/app/app.go b/app/app.go index a80fe6e..f04e730 100644 --- a/app/app.go +++ b/app/app.go @@ -6,20 +6,21 @@ import ( "embed" "errors" "fmt" - "gopher-toolbox/mail" - "gopher-toolbox/utils" + "io" "log/slog" "os" "strings" "time" "aidanwoods.dev/go-paseto" + "github.com/golang-migrate/migrate/v4" _ "github.com/golang-migrate/migrate/v4/database/postgres" "github.com/golang-migrate/migrate/v4/source/iofs" _ "github.com/jackc/pgx/v5/stdlib" ) +// TODO: review consts const ( // Handlers keys InvalidRequest string = "invalid_request" @@ -50,7 +51,7 @@ const ( // User keys UserUsernameKey string = "username_key" UserEmailKey string = "email_key" - UsernameAlReadyExists string = "username_already_exists" + UsernameAlreadyExists string = "username_already_exists" UserSessionKey string = "user_session_key" EmailAlreadyExists string = "email_already_exists" PhoneNumberKey string = "phone_number_key" @@ -60,82 +61,273 @@ const ( LoggedIn string = "logged_in" ) -type App struct { - Database AppDatabase - Security AppSecurity - AppInfo AppInfo - Mailer mail.Mailer -} +var ( + logFile *os.File + logLevel string +) -type AppDatabase struct { - DriverName string - DataSource string - Migrate bool -} +type Config struct { + // default "" + Name string -type AppInfo struct { - Name string + // default "" Version string + + // default ".env" + EnvDirectory string + + // default "development" + EnvMode string + + // default "debug" + LogLevel string + + // default "UTC" + Timezone string + + // default nil + Paseto *Paseto + + // default "" + SMTPHost string + + // default "" + SMTPPort string + + // default "" + SMTPUser string + + // default "" + SMTPPass string + + // default "" + DatabaseDriverName string + + // default "" + DatabaseDataSource string + + // default false + DatabaseMigrate bool } -type AppSecurity struct { +type App struct { + config Config +} + +type Paseto struct { AsymmetricKey paseto.V4AsymmetricSecretKey PublicKey paseto.V4AsymmetricPublicKey Duration time.Duration } -func New(name, version, envDirectory string) *App { - var err error +func New(config ...Config) *App { + cfg := Config{ + Name: "", + Version: "", + EnvDirectory: ".env", + EnvMode: "development", + LogLevel: "debug", + Timezone: "UTC", + Paseto: nil, + SMTPHost: "", + SMTPPort: "", + SMTPUser: "", + SMTPPass: "", + DatabaseDriverName: "pgx", + DatabaseDataSource: "", + DatabaseMigrate: false, + } - err = loadEnvFile(envDirectory) + if len(config) > 0 { + cfg = config[0] + + if cfg.EnvDirectory == "" { + cfg.EnvDirectory = ".env" + } + if cfg.EnvMode == "" { + cfg.EnvMode = "development" + } + if cfg.LogLevel == "" { + cfg.LogLevel = "debug" + } + if cfg.Timezone == "" { + cfg.Timezone = "UTC" + } + if cfg.DatabaseDriverName == "" { + cfg.DatabaseDriverName = "pgx" + } + } + + envDir := os.Getenv("ENV_DIRECTORY") + if envDir == "" { + envDir = cfg.EnvDirectory + } + + err := loadEnvFile(envDir) if err != nil { - slog.Error("error loading env file, using default values", "error", err) + slog.Error("error loading env file", "error", err, "directory", envDir) } - var durationTime time.Duration - var ak paseto.V4AsymmetricSecretKey + if cfg.Name == "" && os.Getenv("APP_NAME") != "" { + cfg.Name = os.Getenv("APP_NAME") + } - if os.Getenv("ASYMMETRIC_KEY") != "" { - ak, err = paseto.NewV4AsymmetricSecretKeyFromHex(os.Getenv("ASYMMETRIC_KEY")) - if err != nil { - slog.Error("error creating asymmetric key", "error", err) + if cfg.Version == "" && os.Getenv("APP_VERSION") != "" { + cfg.Version = os.Getenv("APP_VERSION") + } + + if cfg.EnvMode == "" && os.Getenv("ENV_MODE") != "" { + cfg.EnvMode = os.Getenv("ENV_MODE") + } + + if cfg.LogLevel == "" && os.Getenv("LOG_LEVEL") != "" { + cfg.LogLevel = os.Getenv("LOG_LEVEL") + logLevel = cfg.LogLevel + } + + if cfg.Timezone == "" && os.Getenv("TIMEZONE") != "" { + cfg.Timezone = os.Getenv("TIMEZONE") + } + + loc, err := time.LoadLocation(cfg.Timezone) + if err != nil { + slog.Error("error loading timezone", "error", err, "timezone", cfg.Timezone) + loc = time.UTC + } + time.Local = loc + + startRotativeLogger() + + if cfg.Paseto == nil { + var ak paseto.V4AsymmetricSecretKey + var err error + + if os.Getenv("PASETO_ASYMMETRIC_KEY") != "" { + slog.Info("using paseto asymmetric key from env") + ak, err = paseto.NewV4AsymmetricSecretKeyFromHex(os.Getenv("PASETO_ASYMMETRIC_KEY")) + if err != nil { + slog.Error("error creating asymmetric key", "error", err) + ak = paseto.NewV4AsymmetricSecretKey() + } + } else { + ak = paseto.NewV4AsymmetricSecretKey() } - } else { - ak = paseto.NewV4AsymmetricSecretKey() - } - pk := ak.Public() + pk := ak.Public() - duration := os.Getenv("DURATION") - durationTime = time.Hour * 24 * 7 - if duration != "" { - if parsed, err := time.ParseDuration(duration); err == nil { - durationTime = parsed + duration := time.Hour * 24 * 7 // 7 días por defecto + if os.Getenv("PASETO_DURATION") != "" { + durationStr := os.Getenv("PASETO_DURATION") + durationInt, err := time.ParseDuration(durationStr) + if err != nil { + slog.Error("error parsing PASETO_DURATION", "error", err, "duration", durationStr) + } else { + duration = durationInt + } } - } - return &App{ - Mailer: mail.New( - os.Getenv("SMTP_HOST"), - os.Getenv("SMTP_PORT"), - os.Getenv("SMTP_USER"), - os.Getenv("SMTP_PASS"), - ), - Database: AppDatabase{ - Migrate: utils.GetBool(os.Getenv("MIGRATE")), - DriverName: os.Getenv("DRIVERNAME"), - DataSource: os.Getenv("DATASOURCE"), - }, - Security: AppSecurity{ + cfg.Paseto = &Paseto{ AsymmetricKey: ak, PublicKey: pk, - Duration: durationTime, - }, - AppInfo: AppInfo{ - Name: name, - Version: version, - }, + Duration: duration, + } } + + if cfg.SMTPHost == "" && os.Getenv("SMTP_HOST") != "" { + cfg.SMTPHost = os.Getenv("SMTP_HOST") + } + + if cfg.SMTPPort == "" && os.Getenv("SMTP_PORT") != "" { + cfg.SMTPPort = os.Getenv("SMTP_PORT") + } + + if cfg.SMTPUser == "" && os.Getenv("SMTP_USER") != "" { + cfg.SMTPUser = os.Getenv("SMTP_USER") + } + + if cfg.SMTPPass == "" && os.Getenv("SMTP_PASS") != "" { + cfg.SMTPPass = os.Getenv("SMTP_PASS") + } + + if cfg.DatabaseDriverName == "" && os.Getenv("DATABASE_DRIVER_NAME") != "" { + cfg.DatabaseDriverName = os.Getenv("DATABASE_DRIVER_NAME") + } + + if strings.HasPrefix(cfg.DatabaseDataSource, "OVERRIDE_") { + envKey := strings.TrimPrefix(cfg.DatabaseDataSource, "OVERRIDE_") + if envValue := os.Getenv(envKey); envValue != "" { + slog.Info("using override database data source", "key", envKey) + cfg.DatabaseDataSource = envValue + } else { + slog.Warn("override database data source key not found in environment", "key", envKey) + if os.Getenv("DATABASE_DATA_SOURCE") != "" { + cfg.DatabaseDataSource = os.Getenv("DATABASE_DATA_SOURCE") + } + } + } else if cfg.DatabaseDataSource == "" && os.Getenv("DATABASE_DATA_SOURCE") != "" { + cfg.DatabaseDataSource = os.Getenv("DATABASE_DATA_SOURCE") + } + + if !cfg.DatabaseMigrate && os.Getenv("DATABASE_MIGRATE") == "true" { + cfg.DatabaseMigrate = true + } + + app := &App{ + config: cfg, + } + + slog.Info( + "app config", + "name", cfg.Name, + "version", cfg.Version, + "env_directory", cfg.EnvDirectory, + "env_mode", cfg.EnvMode, + "log_level", cfg.LogLevel, + "timezone", cfg.Timezone, + "paseto_public_key", cfg.Paseto.PublicKey.ExportHex(), + "paseto_duration", cfg.Paseto.Duration.String(), + "smtp_host", cfg.SMTPHost, + "smtp_port", cfg.SMTPPort, + "smtp_user", cfg.SMTPUser, + "smtp_pass", cfg.SMTPPass, + "database_driver", cfg.DatabaseDriverName, + "database_source", cfg.DatabaseDataSource, + "database_migrate", cfg.DatabaseMigrate, + ) + + return app +} + +func (a *App) Name() string { + return a.config.Name +} + +func (a *App) Version() string { + return a.config.Version +} + +func (a *App) EnvMode() string { + return a.config.EnvMode +} + +func (a *App) LogLevel() string { + return a.config.LogLevel +} + +func (a *App) Paseto() *Paseto { + return a.config.Paseto +} + +func (a *App) SMTPConfig() (host, port, user, pass string) { + return a.config.SMTPHost, a.config.SMTPPort, a.config.SMTPUser, a.config.SMTPPass +} + +func (a *App) DatabaseDataSource() string { + return a.config.DatabaseDataSource +} + +func (a *App) Timezone() string { + return a.config.Timezone } // MigrateDB migrates the database. The migrations must stored in the @@ -145,11 +337,11 @@ func New(name, version, envDirectory string) *App { // // cmd/database/migrations/*.sql func (a *App) Migrate(database embed.FS) { - if a.Database.Migrate == false { + if a.config.DatabaseMigrate == false { slog.Info("migration disabled") return } - dbConn, err := sql.Open(a.Database.DriverName, a.Database.DataSource) + dbConn, err := sql.Open(a.config.DatabaseDriverName, a.config.DatabaseDataSource) if err != nil { fmt.Println(err) return @@ -162,7 +354,7 @@ func (a *App) Migrate(database embed.FS) { return } - m, err := migrate.NewWithSourceInstance("iofs", d, a.Database.DataSource) + m, err := migrate.NewWithSourceInstance("iofs", d, a.config.DatabaseDataSource) if err != nil { fmt.Println(err) return @@ -204,3 +396,60 @@ func loadEnvFile(envDirectory string) error { } return scanner.Err() } + +func slogLevelType(level string) slog.Level { + var logLevel slog.Level + + switch strings.ToLower(level) { + case "debug": + logLevel = slog.LevelDebug + case "info": + logLevel = slog.LevelInfo + case "warn": + logLevel = slog.LevelWarn + case "error": + logLevel = slog.LevelError + default: + logLevel = slog.LevelInfo + } + + return logLevel +} + +func newLogger(level slog.Level) { + if err := os.MkdirAll("logs", 0755); err != nil { + fmt.Println("error creating logs directory:", err) + return + } + + now := time.Now().Format("2006-01-02") + f, err := os.OpenFile(fmt.Sprintf("logs/log%s.log", now), os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) + if err != nil { + fmt.Println("error opening log file:", err) + return + } + + mw := io.MultiWriter(os.Stdout, f) + logger := slog.New(slog.NewTextHandler(mw, &slog.HandlerOptions{ + AddSource: true, + Level: level, + })) + + if logFile != nil { + logFile.Close() // Cierra el archivo anterior antes de rotar + } + + logFile = f + slog.SetDefault(logger) +} + +func startRotativeLogger() { + newLogger(slogLevelType(logLevel)) + + ticker := time.NewTicker(time.Hour * 24) + go func() { + for range ticker.C { + newLogger(slogLevelType(logLevel)) + } + }() +} diff --git a/esfaker/esfaker.go b/esfaker/esfaker.go deleted file mode 100644 index 64fa45d..0000000 --- a/esfaker/esfaker.go +++ /dev/null @@ -1,116 +0,0 @@ -package esfaker - -import ( - "math/rand" - "strings" -) - -const uppercaseAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" -const lowercaseAlphabet = "abcdefghijklmnopqrstuvwxyz" -const numbers = "0123456789" -const symbols = "!@#$%^&*()_+{}|:<>?~" - -var maleNames = []string{ - "Pedro", "Juan", "Pepe", "Francisco", "Luis", "Carlos", "Javier", "José", "Antonio", "Manuel", -} -var femaleNames = []string{ - "María", "Ana", "Isabel", "Laura", "Carmen", "Rosa", "Julia", "Elena", "Sara", "Lucía", -} -var lastNames = []string{ - "García", "Fernández", "González", "Rodríguez", "López", "Martínez", "Sánchez", "Pérez", "Gómez", "Martín", -} - -func MaleName() string { - return maleNames[rand.Intn(len(maleNames))] -} - -func FemaleName() string { - return femaleNames[rand.Intn(len(femaleNames))] -} - -func Name() string { - allNames := append(maleNames, femaleNames...) - return allNames[rand.Intn(len(allNames))] -} - -func LastName() string { - return lastNames[rand.Intn(len(lastNames))] -} - -func Email(beforeAt string) string { - return beforeAt + "@" + Chars(5, 10) + ".local" -} - -func Int(min, max int64) int64 { - return min + rand.Int63n(max-min+1) -} - -func Float(min, max float64) float64 { - return min + rand.Float64()*(max-min) -} - -func Bool() bool { - return rand.Intn(2) == 0 -} - -func Chars(min, max int) string { - var sb strings.Builder - k := len(lowercaseAlphabet) - - for i := 0; i < rand.Intn(max-min+1)+min; i++ { - c := lowercaseAlphabet[rand.Intn(k)] - sb.WriteByte(c) - } - - return sb.String() -} - -func AllChars(min, max int) string { - allChars := uppercaseAlphabet + lowercaseAlphabet + numbers + symbols - var sb strings.Builder - k := len(allChars) - - for i := 0; i < rand.Intn(max-min+1)+min; i++ { - c := allChars[rand.Intn(k)] - sb.WriteByte(c) - } - - return sb.String() -} - -func AllCharsOrEmpty(min, max int) string { - if Bool() { - return "" - } - return AllChars(min, max) -} - -func AllCharsOrNil(min, max int) *string { - if Bool() { - return nil - } - s := AllChars(min, max) - return &s -} - -func NumericString(length int) string { - var sb strings.Builder - - for i := 0; i < length; i++ { - sb.WriteByte(numbers[rand.Intn(len(numbers))]) - } - - return sb.String() -} - -func Sentence(min, max int) string { - var sb strings.Builder - k := len(lowercaseAlphabet) - - for i := 0; i < rand.Intn(max-min+1)+min; i++ { - c := lowercaseAlphabet[rand.Intn(k)] - sb.WriteByte(c) - } - - return sb.String() -} diff --git a/templates/functions.go b/templates/functions.go new file mode 100644 index 0000000..b9da1a2 --- /dev/null +++ b/templates/functions.go @@ -0,0 +1,34 @@ +package templates + +import ( + "errors" + "strconv" + "time" +) + +func Dict(values ...interface{}) (map[string]interface{}, error) { + if len(values)%2 != 0 { + return nil, errors.New("invalid dict call") + } + dict := make(map[string]interface{}, len(values)/2) + for i := 0; i < len(values); i += 2 { + key, ok := values[i].(string) + if !ok { + return nil, errors.New("dict keys must be strings") + } + dict[key] = values[i+1] + } + return dict, nil +} + +func FormatDateSpanish(date time.Time) string { + months := []string{"enero", "febrero", "marzo", "abril", "mayo", "junio", "julio", "agosto", "septiembre", "octubre", "noviembre", "diciembre"} + days := []string{"domingo", "lunes", "martes", "miércoles", "jueves", "viernes", "sábado"} + + dayName := days[date.Weekday()] + day := date.Day() + month := months[date.Month()-1] + year := date.Year() + + return dayName + ", " + strconv.Itoa(day) + " de " + month + " de " + strconv.Itoa(year) +} diff --git a/utils/utils.go b/utils/utils.go index 5a67646..fa9fa4d 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -4,7 +4,6 @@ import ( "fmt" "log/slog" "regexp" - "strconv" "strings" "time" "unicode" @@ -58,19 +57,3 @@ func Slugify(s string) string { return s } - -func isMn(r rune) bool { - return unicode.Is(unicode.Mn, r) -} - -func FormatDateSpanish(date time.Time) string { - months := []string{"enero", "febrero", "marzo", "abril", "mayo", "junio", "julio", "agosto", "septiembre", "octubre", "noviembre", "diciembre"} - days := []string{"domingo", "lunes", "martes", "miércoles", "jueves", "viernes", "sábado"} - - dayName := days[date.Weekday()] - day := date.Day() - month := months[date.Month()-1] - year := date.Year() - - return dayName + ", " + strconv.Itoa(day) + " de " + month + " de " + strconv.Itoa(year) -}