1dfccf513a
- Initialize slog in the serve command with terminal/file support - Introduce `AppError` with HTTP status for unified service-layer errors - Replace ad-hoc `api.Error` calls with `api.RespondError` - Wrap internal errors with `model.NewInternalError` and add reference IDs - Add `RequestID` middleware and switch router from `gin.Default` to `gin.New`
150 lines
4.1 KiB
Go
150 lines
4.1 KiB
Go
package config
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"net"
|
|
"time"
|
|
)
|
|
|
|
type Config struct {
|
|
Server ServerConfig `mapstructure:"server"`
|
|
Database DatabaseConfig `mapstructure:"database"`
|
|
Storage StorageConfig `mapstructure:"storage"`
|
|
JWT JWTConfig `mapstructure:"jwt"`
|
|
Log LogConfig `mapstructure:"log"`
|
|
}
|
|
|
|
type ServerConfig struct {
|
|
Host string `mapstructure:"host"`
|
|
Port int `mapstructure:"port"`
|
|
}
|
|
|
|
type DatabaseConfig struct {
|
|
Driver string `mapstructure:"driver"`
|
|
SQLite SQLiteConfig `mapstructure:"sqlite"`
|
|
Postgres PostgresConfig `mapstructure:"postgres"`
|
|
}
|
|
|
|
type SQLiteConfig struct {
|
|
Path string `mapstructure:"path"`
|
|
}
|
|
|
|
type PostgresConfig struct {
|
|
Host string `mapstructure:"host"`
|
|
Port int `mapstructure:"port"`
|
|
User string `mapstructure:"user"`
|
|
Password string `mapstructure:"password"`
|
|
DBName string `mapstructure:"dbname"`
|
|
SSLMode string `mapstructure:"sslmode"`
|
|
}
|
|
|
|
type StorageConfig struct {
|
|
Driver string `mapstructure:"driver"`
|
|
Local LocalStorageConfig `mapstructure:"local"`
|
|
MaxUploadSize int64 `mapstructure:"max_upload_size"`
|
|
}
|
|
|
|
type LocalStorageConfig struct {
|
|
Path string `mapstructure:"path"`
|
|
}
|
|
|
|
type JWTConfig struct {
|
|
Secret string `mapstructure:"secret"`
|
|
AccessTTL time.Duration `mapstructure:"access_ttl"`
|
|
RefreshTTL time.Duration `mapstructure:"refresh_ttl"`
|
|
}
|
|
|
|
type LogConfig struct {
|
|
Level string `mapstructure:"level"`
|
|
FilePath string `mapstructure:"file_path"`
|
|
FileLevel string `mapstructure:"file_level"`
|
|
}
|
|
|
|
// ParseLogLevel converts a string to slog.Level.
|
|
func ParseLogLevel(s string) (slog.Level, error) {
|
|
if s == "" {
|
|
return slog.LevelInfo, nil
|
|
}
|
|
switch s {
|
|
case "debug":
|
|
return slog.LevelDebug, nil
|
|
case "info":
|
|
return slog.LevelInfo, nil
|
|
case "warn":
|
|
return slog.LevelWarn, nil
|
|
case "error":
|
|
return slog.LevelError, nil
|
|
default:
|
|
return slog.LevelInfo, fmt.Errorf("unknown log level: %q (use debug, info, warn, or error)", s)
|
|
}
|
|
}
|
|
func (c *Config) Validate() error {
|
|
var errs []error
|
|
|
|
if c.Server.Port < 1 || c.Server.Port > 65535 {
|
|
errs = append(errs, fmt.Errorf("server.port: %d out of range [1, 65535]", c.Server.Port))
|
|
}
|
|
|
|
if addr := net.ParseIP(c.Server.Host); c.Server.Host != "" && addr == nil {
|
|
errs = append(errs, fmt.Errorf("server.host: %q is not a valid IP address", c.Server.Host))
|
|
}
|
|
|
|
switch c.Database.Driver {
|
|
case "sqlite3":
|
|
if c.Database.SQLite.Path == "" {
|
|
errs = append(errs, errors.New("database.sqlite.path: must not be empty"))
|
|
}
|
|
case "postgres":
|
|
if c.Database.Postgres.Host == "" {
|
|
errs = append(errs, errors.New("database.postgres.host: must not be empty"))
|
|
}
|
|
if c.Database.Postgres.Port < 1 || c.Database.Postgres.Port > 65535 {
|
|
errs = append(errs, fmt.Errorf("database.postgres.port: %d out of range [1, 65535]", c.Database.Postgres.Port))
|
|
}
|
|
if c.Database.Postgres.User == "" {
|
|
errs = append(errs, errors.New("database.postgres.user: must not be empty"))
|
|
}
|
|
if c.Database.Postgres.DBName == "" {
|
|
errs = append(errs, errors.New("database.postgres.dbname: must not be empty"))
|
|
}
|
|
default:
|
|
errs = append(errs, fmt.Errorf("database.driver: %q is not supported (use sqlite3 or postgres)", c.Database.Driver))
|
|
}
|
|
|
|
if c.Storage.Local.Path == "" {
|
|
errs = append(errs, errors.New("storage.local.path: must not be empty"))
|
|
}
|
|
|
|
if c.Storage.MaxUploadSize < 0 {
|
|
errs = append(errs, errors.New("storage.max_upload_size: must not be negative"))
|
|
}
|
|
|
|
if c.JWT.Secret == "" {
|
|
errs = append(errs, errors.New("jwt.secret: must not be empty"))
|
|
}
|
|
|
|
if c.JWT.AccessTTL <= 0 {
|
|
errs = append(errs, errors.New("jwt.access_ttl: must be positive"))
|
|
}
|
|
|
|
if c.JWT.RefreshTTL <= 0 {
|
|
errs = append(errs, errors.New("jwt.refresh_ttl: must be positive"))
|
|
}
|
|
|
|
if c.Log.Level != "" {
|
|
if _, err := ParseLogLevel(c.Log.Level); err != nil {
|
|
errs = append(errs, fmt.Errorf("log.level: %w", err))
|
|
}
|
|
}
|
|
|
|
if c.Log.FileLevel != "" {
|
|
if _, err := ParseLogLevel(c.Log.FileLevel); err != nil {
|
|
errs = append(errs, fmt.Errorf("log.file_level: %w", err))
|
|
}
|
|
}
|
|
|
|
return errors.Join(errs...)
|
|
}
|