diff --git a/.gitignore b/.gitignore index 2eea525..c16a7a9 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -.env \ No newline at end of file +.env +.env.test \ No newline at end of file diff --git a/cmd/db/main.go b/cmd/db/main.go index 1015e55..a260f44 100644 --- a/cmd/db/main.go +++ b/cmd/db/main.go @@ -11,85 +11,103 @@ import ( "github.com/spf13/cobra" ) -var rootCmd = &cobra.Command{ - Use: "db", - Short: "Database management CLI", - Long: `Manage database: create, drop, migrate, seed, etc.`, - PersistentPreRun: func(cmd *cobra.Command, args []string) { - config.Load() - }, -} +func main() { + cfg, _ := config.Load() + rootCmd := NewRootCmd() + dbCreateCmd := NewDbCreateCmd(&cfg.Database) + dbDropCmd := NewDbDropCmd(&cfg.Database) + migrationCreateCmd := NewMigrationCreateCmd(&cfg.Database) + migrationUpCmd := NewMigrationUpCmd(cfg) + migrationDownCmd := NewMigrationDownCmd(cfg) + migrationStatusCmd := NewMigrationStatucCmd(cfg) -var dbCreateCmd = &cobra.Command{ - Use: "db-create", - Short: "Create database", - Run: func(cmd *cobra.Command, args []string) { - if err := admin.CreateDatabase(); err != nil { - log.Fatal("Failed to create database. ", err) - } - }, -} + rootCmd.AddCommand(dbCreateCmd, dbDropCmd, migrationCreateCmd, migrationUpCmd, migrationDownCmd, migrationStatusCmd) -var dbDropCmd = &cobra.Command{ - Use: "db-drop", - Short: "Drop database", - Run: func(cmd *cobra.Command, args []string) { - if err := admin.DropDatabase(); err != nil { - log.Fatal("Failed to drop database. ", err) - } - }, + if err := rootCmd.Execute(); err != nil { + log.Fatal(err) + } } -var migrationCreateCmd = &cobra.Command{ - Use: "migration-create", - Short: "Create migration", - Args: cobra.ExactArgs(1), - Run: func(cmd *cobra.Command, args []string) { - fmt.Println(args) - if err := migrate.Create(args[0]); err != nil { - log.Fatal("Failed to create migration. ", err) - } +func NewRootCmd() *cobra.Command { + return &cobra.Command{ + Use: "db", + Short: "Database management CLI", + Long: `Manage database: create, drop, migrate, seed, etc.`, + } +} - log.Println("✓ Migration files created") - }, +func NewDbCreateCmd(dbConfig *config.DatabaseConfig) *cobra.Command { + return &cobra.Command{ + Use: "db-create", + Short: "Create database", + Run: func(cmd *cobra.Command, args []string) { + if err := admin.CreateDatabase(dbConfig); err != nil { + log.Fatal("Failed to create database. ", err) + } + }, + } } -var migrationUpCmd = &cobra.Command{ - Use: "migration-up", - Short: "Apply all migrations", - Run: func(cmd *cobra.Command, args []string) { - if err := migrate.Up(); err != nil { - log.Fatal("Failed apply all migrations. ", err) - } - }, +func NewDbDropCmd(dbConfig *config.DatabaseConfig) *cobra.Command { + return &cobra.Command{ + Use: "db-drop", + Short: "Drop database", + Run: func(cmd *cobra.Command, args []string) { + if err := admin.DropDatabase(dbConfig); err != nil { + log.Fatal("Failed to drop database. ", err) + } + }, + } } -var migrationDownCmd = &cobra.Command{ - Use: "migration-down", - Short: "Rollback to previous migration", - Run: func(cmd *cobra.Command, args []string) { - if err := migrate.Down(); err != nil { - log.Fatal("Failed rollback to previous migration. ", err) - } - }, +func NewMigrationCreateCmd(dbConfig *config.DatabaseConfig) *cobra.Command { + return &cobra.Command{ + Use: "migration-create", + Short: "Create migration", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + fmt.Println(args) + if err := migrate.Create(args[0], dbConfig); err != nil { + log.Fatal("Failed to create migration. ", err) + } + + log.Println("✓ Migration files created") + }, + } } -var migrationStatusCmd = &cobra.Command{ - Use: "migration-status", - Short: "Print the status of all migrations", - Run: func(cmd *cobra.Command, args []string) { - if err := migrate.Status(); err != nil { - log.Fatal("Failed print the status of all migrations. ", err) - } - }, +func NewMigrationUpCmd(cfg *config.Config) *cobra.Command { + return &cobra.Command{ + Use: "migration-up", + Short: "Apply all migrations", + Run: func(cmd *cobra.Command, args []string) { + if err := migrate.Up(cfg); err != nil { + log.Fatal("Failed apply all migrations. ", err) + } + }, + } } -func init() { - rootCmd.AddCommand(dbCreateCmd, dbDropCmd, migrationCreateCmd, migrationUpCmd, migrationDownCmd, migrationStatusCmd) +func NewMigrationDownCmd(cfg *config.Config) *cobra.Command { + return &cobra.Command{ + Use: "migration-down", + Short: "Rollback to previous migration", + Run: func(cmd *cobra.Command, args []string) { + if err := migrate.Down(cfg); err != nil { + log.Fatal("Failed rollback to previous migration. ", err) + } + }, + } } -func main() { - if err := rootCmd.Execute(); err != nil { - log.Fatal(err) +func NewMigrationStatucCmd(cfg *config.Config) *cobra.Command { + return &cobra.Command{ + Use: "migration-status", + Short: "Print the status of all migrations", + Run: func(cmd *cobra.Command, args []string) { + if err := migrate.Status(cfg); err != nil { + log.Fatal("Failed print the status of all migrations. ", err) + } + }, } } diff --git a/cmd/wasted/main.go b/cmd/wasted/main.go index d9efbd7..6c8d997 100644 --- a/cmd/wasted/main.go +++ b/cmd/wasted/main.go @@ -9,7 +9,6 @@ import ( "github.com/ExplosiveGM/wasted/internal/db/client" "github.com/ExplosiveGM/wasted/internal/logger" _ "github.com/jackc/pgx/v5/stdlib" - "github.com/spf13/viper" ) // @title wasted @@ -26,9 +25,9 @@ import ( // @schemes http https func main() { - config.Load() - - db, err := client.Connect() + cfg, _ := config.Load() + fmt.Println(cfg) + db, err := client.Connect(&cfg.Database) if err != nil { log.Fatalf("❌ Database connection failed: %v", err) @@ -36,16 +35,10 @@ func main() { defer db.Close() - logger := logger.NewLogger(logger.Config{ - Environment: viper.GetString("APP_ENV"), - LogLevel: viper.GetString("LOG_LEVEL"), - LogFile: viper.GetString("LOG_FILE"), - EnableJSON: viper.GetBool("LOG_ENABLE_JSON"), - EnableColor: viper.GetBool("LOG_ENABLE_COLOR"), - }) + logger := logger.NewLogger(cfg) - router := api.Router(db, logger) - logger.Info().Str("env", viper.GetString("APP_ENV")).Msg("🚀 Запуск приложения") + router := api.Router(db, logger, cfg) + logger.Info().Str("env", cfg.App.Env).Msg("🚀 Запуск приложения") if err := router.Run(); err != nil { logger.Panic().Msg( diff --git a/config/config.go b/config/config.go index fd5e7b6..e549772 100644 --- a/config/config.go +++ b/config/config.go @@ -1,39 +1,101 @@ package config import ( + "fmt" "log" "os" "path/filepath" + "strings" "github.com/spf13/viper" ) +type Config struct { + App AppConfig `mapstructure:",squash"` + Database DatabaseConfig `mapstructure:",squash"` + Jwt JWTConfig `mapstructure:",squash"` + Log LogConfig `mapstructure:",squash"` + Path PathConfig +} + +type AppConfig struct { + Name string `mapstructure:"APP_NAME"` + Env string `mapstructure:"APP_ENV"` + Port string `mapstructure:"APP_PORT"` +} + +type DatabaseConfig struct { + Host string `mapstructure:"DB_HOST"` + Port string `mapstructure:"DB_PORT"` + User string `mapstructure:"DB_USER"` + Password string `mapstructure:"DB_PASSWORD"` + Name string `mapstructure:"DB_NAME"` + SslMode string `mapstructure:"DB_SSL_MODE"` +} + +type JWTConfig struct { + AccessSecret string `mapstructure:"JWT_ACCESS_SECRET"` + RefreshSecret string `mapstructure:"JWT_REFRESH_SECRET"` +} + +type LogConfig struct { + Level string `mapstructure:"LOG_LEVEL"` + File string `mapstructure:"LOG_FILE"` + EnableJson bool `mapstructure:"LOG_ENABLE_JSON"` + EnableColor bool `mapstructure:"LOG_ENABLE_COLOR"` +} + +type PathConfig struct { + RootDir string + DBDir string + DBMigrations string +} + var ( RootDir string DBDir string DBMigrations string ) -func Load() { - RootDir = findProjectRootByGoMod() - DBDir = filepath.Join(RootDir, "db") - DBMigrations = filepath.Join(DBDir, "migrations") +func Load() (*Config, error) { + env := os.Getenv("APP_ENV") + + if strings.ToLower(env) == "test" { + viper.SetConfigName(".env.test") + } else { + viper.SetConfigName(".env") + } - viper.SetConfigName(".env") viper.SetConfigType("env") viper.AddConfigPath(".") viper.AddConfigPath("./config") + viper.AutomaticEnv() + var cfg Config if err := viper.ReadInConfig(); err != nil { if _, ok := err.(viper.ConfigFileNotFoundError); ok { log.Println(".env file not found, using environment variables") } else { log.Fatalf("Error reading config file: %v", err) } + } else if err := viper.Unmarshal(&cfg); err != nil { + return nil, fmt.Errorf("unmarshal config: %w", err) } - viper.AutomaticEnv() + setPaths(&cfg.Path) + + return &cfg, nil +} + +func setPaths(pathConfig *PathConfig) { + rootDir := findProjectRootByGoMod() + dbDir := filepath.Join(rootDir, "db") + dbMigrations := filepath.Join(dbDir, "migrations") + + pathConfig.RootDir = rootDir + pathConfig.DBDir = dbDir + pathConfig.DBMigrations = dbMigrations } func findProjectRootByGoMod() string { @@ -57,8 +119,3 @@ func findProjectRootByGoMod() string { return "" } - -func findEnvFile() { - viper.SetConfigName(".env") - viper.SetConfigType("env") -} diff --git a/internal/api/router.go b/internal/api/router.go index 7ed733f..3da624d 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -3,6 +3,7 @@ package api import ( "database/sql" + "github.com/ExplosiveGM/wasted/config" "github.com/ExplosiveGM/wasted/docs" "github.com/ExplosiveGM/wasted/internal/auth" "github.com/ExplosiveGM/wasted/internal/database" @@ -12,7 +13,7 @@ import ( ginSwagger "github.com/swaggo/gin-swagger" ) -func Router(db *sql.DB, logger zerolog.Logger) *gin.Engine { +func Router(db *sql.DB, logger zerolog.Logger, cfg *config.Config) *gin.Engine { router := gin.Default() docs.SwaggerInfo.BasePath = "/api/v1" v1 := router.Group("/api/v1") @@ -20,7 +21,7 @@ func Router(db *sql.DB, logger zerolog.Logger) *gin.Engine { authRoute := v1.Group("/auth") { queries := database.New(db) - authService := auth.NewAuthService(queries, logger) + authService := auth.NewAuthService(queries, logger, &cfg.Jwt) authHandler := auth.NewAuthHandler(authService) authRoute.POST("/request-code", authHandler.RequestCode) authRoute.POST("/verify", authHandler.Verify) diff --git a/internal/auth/service.go b/internal/auth/service.go index 0d96913..ec4212e 100644 --- a/internal/auth/service.go +++ b/internal/auth/service.go @@ -8,6 +8,7 @@ import ( "strconv" "time" + "github.com/ExplosiveGM/wasted/config" "github.com/ExplosiveGM/wasted/internal/database" "github.com/ExplosiveGM/wasted/internal/messaging" "github.com/ExplosiveGM/wasted/internal/utils" @@ -16,12 +17,13 @@ import ( ) type Service struct { - queries *database.Queries - logger zerolog.Logger + queries *database.Queries + logger zerolog.Logger + jwtConfig *config.JWTConfig } -func NewAuthService(queries *database.Queries, logger zerolog.Logger) *Service { - return &Service{queries: queries, logger: logger} +func NewAuthService(queries *database.Queries, logger zerolog.Logger, jwtConfig *config.JWTConfig) *Service { + return &Service{queries: queries, logger: logger, jwtConfig: jwtConfig} } func (s *Service) RequestCode(ctx context.Context, login string) error { @@ -118,14 +120,14 @@ func (s *Service) Verify(ctx context.Context, login string, code int) (TokenPair return TokenPair{}, fmt.Errorf("%w: %v", ErrUserWithCodeNotFound, err) } - accessTokenParams, err := generateAccessToken(user.Login) + accessTokenParams, err := generateAccessToken(user.Login, s.jwtConfig) if err != nil { s.logger.Err(err).Msg("Ошибка при генерации access-токена") return TokenPair{}, fmt.Errorf("%w: %v", ErrGeneratingAccessToken, err) } - refreshTokenParams, err := generateRefreshToken(user.Login) + refreshTokenParams, err := generateRefreshToken(user.Login, s.jwtConfig) if err != nil { s.logger.Err(err).Msg("Ошибка при генерации refresh-токена") @@ -162,7 +164,7 @@ func (s *Service) Refresh(ctx context.Context, refreshToken string) (RefreshResu return RefreshResult{}, fmt.Errorf("%w: %v", ErrRefreshTokenNotFound, err) } - accessTokenParams, err := generateAccessToken(user.Login) + accessTokenParams, err := generateAccessToken(user.Login, s.jwtConfig) if err != nil { return RefreshResult{}, fmt.Errorf("%w: %v", ErrGeneratingAccessToken, err) diff --git a/internal/auth/token_manager.go b/internal/auth/token_manager.go index ebdebd7..71734e8 100644 --- a/internal/auth/token_manager.go +++ b/internal/auth/token_manager.go @@ -6,8 +6,8 @@ import ( "fmt" "time" + "github.com/ExplosiveGM/wasted/config" "github.com/golang-jwt/jwt/v5" - "github.com/spf13/viper" ) type TokenParams struct { @@ -15,16 +15,16 @@ type TokenParams struct { ttl time.Time } -func generateAccessToken(login string) (*TokenParams, error) { +func generateAccessToken(login string, jwtConfig *config.JWTConfig) (*TokenParams, error) { ttl := time.Now().Add(15 * time.Minute) - secretKey := viper.GetString("JWT_ACCESS_SECRET") + secretKey := jwtConfig.AccessSecret return generateToken(login, secretKey, ttl) } -func generateRefreshToken(login string) (*TokenParams, error) { +func generateRefreshToken(login string, jwtConfig *config.JWTConfig) (*TokenParams, error) { ttl := time.Now().Add(7 * 24 * time.Hour) - secretKey := viper.GetString("JWT_REFRESH_SECRET") + secretKey := jwtConfig.RefreshSecret return generateToken(login, secretKey, ttl) } diff --git a/internal/db/admin/admin.go b/internal/db/admin/admin.go index 1bcbc92..9a0b6b6 100644 --- a/internal/db/admin/admin.go +++ b/internal/db/admin/admin.go @@ -4,17 +4,17 @@ import ( "fmt" "log" - "github.com/spf13/viper" + "github.com/ExplosiveGM/wasted/config" ) -func CreateDatabase() error { - adminDB, err := Connect() +func CreateDatabase(dbConfig *config.DatabaseConfig) error { + adminDB, err := Connect(dbConfig) if err != nil { return fmt.Errorf("connect to admin db: %w", err) } defer adminDB.Close() - dbName := viper.GetString("DB_NAME") + dbName := dbConfig.Name var exists bool query := `SELECT EXISTS(SELECT 1 FROM pg_database WHERE datname = $1)` @@ -40,14 +40,14 @@ func CreateDatabase() error { return nil } -func DropDatabase() error { - adminDB, err := Connect() +func DropDatabase(dbConfig *config.DatabaseConfig) error { + adminDB, err := Connect(dbConfig) if err != nil { return fmt.Errorf("connect to admin db: %w", err) } defer adminDB.Close() - dbName := viper.GetString("DB_NAME") + dbName := dbConfig.Name _, err = adminDB.Exec(` SELECT pg_terminate_backend(pg_stat_activity.pid) diff --git a/internal/db/admin/connection.go b/internal/db/admin/connection.go index 2279d7e..2e2dc99 100644 --- a/internal/db/admin/connection.go +++ b/internal/db/admin/connection.go @@ -4,17 +4,17 @@ import ( "database/sql" "fmt" - "github.com/spf13/viper" + "github.com/ExplosiveGM/wasted/config" ) -func Connect() (*sql.DB, error) { +func Connect(dbConfig *config.DatabaseConfig) (*sql.DB, error) { connStr := fmt.Sprintf( "postgres://%s:%s@%s:%s/postgres?sslmode=%s", - viper.GetString("DB_USER"), - viper.GetString("DB_PASSWORD"), - viper.GetString("DB_HOST"), - viper.GetString("DB_PORT"), - viper.GetString("DB_SSLMODE"), + dbConfig.User, + dbConfig.Password, + dbConfig.Host, + dbConfig.Port, + dbConfig.SslMode, ) return sql.Open("pgx", connStr) diff --git a/internal/db/client/connection.go b/internal/db/client/connection.go index c2674d2..30f8979 100644 --- a/internal/db/client/connection.go +++ b/internal/db/client/connection.go @@ -6,18 +6,18 @@ import ( "log" "time" - "github.com/spf13/viper" + "github.com/ExplosiveGM/wasted/config" ) -func Connect() (*sql.DB, error) { +func Connect(dbConfig *config.DatabaseConfig) (*sql.DB, error) { connStr := fmt.Sprintf( "host=%s port=%s user=%s password=%s dbname=%s sslmode=%s", - viper.GetString("DB_HOST"), - viper.GetString("DB_PORT"), - viper.GetString("DB_USER"), - viper.GetString("DB_PASSWORD"), - viper.GetString("DB_NAME"), - viper.GetString("DB_SSLMODE"), + dbConfig.Host, + dbConfig.Port, + dbConfig.User, + dbConfig.Password, + dbConfig.Name, + dbConfig.SslMode, ) db, err := sql.Open("pgx", connStr) diff --git a/internal/db/migrate/migrate.go b/internal/db/migrate/migrate.go index 2e2f111..a8e3e70 100644 --- a/internal/db/migrate/migrate.go +++ b/internal/db/migrate/migrate.go @@ -8,51 +8,58 @@ import ( "github.com/pressly/goose/v3" ) -func Create(name string) error { - db, err := client.Connect() +func Create(name string, dbConfig *config.DatabaseConfig) error { + db, err := client.Connect(dbConfig) if err != nil { return fmt.Errorf("connect to database: %w", err) } + defer db.Close() return goose.Create(db, config.DBMigrations, name, "sql") } -func Up() error { - db, err := client.Connect() +func Up(cfg *config.Config) error { + db, err := client.Connect(&cfg.Database) + if err != nil { return fmt.Errorf("connect to database: %w", err) } + defer db.Close() - if err := goose.Up(db, config.DBMigrations); err != nil { + if err := goose.Up(db, cfg.Path.DBMigrations); err != nil { return fmt.Errorf("run migrations: %w", err) } return nil } -func Down() error { - db, err := client.Connect() +func Down(cfg *config.Config) error { + db, err := client.Connect(&cfg.Database) + if err != nil { return fmt.Errorf("connect to database: %w", err) } + defer db.Close() - if err := goose.Down(db, config.DBMigrations); err != nil { + if err := goose.Down(db, cfg.Path.DBMigrations); err != nil { return fmt.Errorf("rollback migration: %w", err) } return nil } -func Status() error { - db, err := client.Connect() +func Status(cfg *config.Config) error { + db, err := client.Connect(&cfg.Database) + if err != nil { return fmt.Errorf("connect to database: %w", err) } + defer db.Close() - return goose.Status(db, config.DBMigrations) + return goose.Status(db, cfg.Path.DBMigrations) } diff --git a/internal/logger/factory.go b/internal/logger/factory.go index f9b0403..4c6f491 100644 --- a/internal/logger/factory.go +++ b/internal/logger/factory.go @@ -5,34 +5,26 @@ import ( "os" "strings" + "github.com/ExplosiveGM/wasted/config" "github.com/rs/zerolog" "github.com/rs/zerolog/log" ) -type Config struct { - AppName string - Environment string - LogLevel string - LogFile string - EnableJSON bool - EnableColor bool -} - -func NewLogger(cfg Config) zerolog.Logger { - level := parseLevel(cfg.LogLevel) +func NewLogger(cfg *config.Config) zerolog.Logger { + level := parseLevel(cfg.Log.Level) zerolog.SetGlobalLevel(level) var writers []io.Writer - if cfg.Environment != "production" || cfg.EnableColor { + if cfg.App.Env != "production" || cfg.Log.EnableColor { writers = append(writers, createConsoleWriter(cfg)) } - if cfg.LogFile != "" { - writers = append(writers, createFileWriter(cfg.LogFile)) + if cfg.Log.File != "" { + writers = append(writers, createFileWriter(cfg.Log.File)) } - if cfg.Environment == "production" { + if cfg.App.Env == "production" { writers = append(writers, createSyslogWriter()) } @@ -42,8 +34,8 @@ func NewLogger(cfg Config) zerolog.Logger { With(). Timestamp(). Caller(). - Str("app", cfg.AppName). - Str("env", cfg.Environment). + Str("app", cfg.App.Name). + Str("env", cfg.App.Env). Logger() log.Logger = logger @@ -68,14 +60,14 @@ func parseLevel(level string) zerolog.Level { } } -func createConsoleWriter(cfg Config) io.Writer { - if cfg.EnableJSON { +func createConsoleWriter(cfg *config.Config) io.Writer { + if cfg.Log.EnableJson { return os.Stderr } return zerolog.ConsoleWriter{ Out: os.Stderr, - NoColor: !cfg.EnableColor, + NoColor: !cfg.Log.EnableColor, TimeFormat: "2006-01-02 15:04:05", } }