mirror of
https://github.com/get-drexa/drive.git
synced 2026-02-03 01:01:17 +00:00
feat: impl config loading
This commit is contained in:
155
apps/backend/internal/drexa/config.go
Normal file
155
apps/backend/internal/drexa/config.go
Normal file
@@ -0,0 +1,155 @@
|
||||
package drexa
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type StorageMode string
|
||||
type StorageBackend string
|
||||
|
||||
const (
|
||||
StorageModeFlat StorageMode = "flat"
|
||||
StorageModeHierarchical StorageMode = "hierarchical"
|
||||
)
|
||||
|
||||
const (
|
||||
StorageBackendFS StorageBackend = "fs"
|
||||
StorageBackendS3 StorageBackend = "s3"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Server ServerConfig `yaml:"server"`
|
||||
Database DatabaseConfig `yaml:"database"`
|
||||
JWT JWTConfig `yaml:"jwt"`
|
||||
Storage StorageConfig `yaml:"storage"`
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
Port int `yaml:"port"`
|
||||
}
|
||||
|
||||
type DatabaseConfig struct {
|
||||
PostgresURL string `yaml:"postgres_url"`
|
||||
}
|
||||
|
||||
type JWTConfig struct {
|
||||
Issuer string `yaml:"issuer"`
|
||||
Audience string `yaml:"audience"`
|
||||
SecretKeyBase64 string `yaml:"secret_key_base64"`
|
||||
SecretKeyPath string `yaml:"secret_key_path"`
|
||||
SecretKey []byte `yaml:"-"`
|
||||
}
|
||||
|
||||
type StorageConfig struct {
|
||||
Mode StorageMode `yaml:"mode"`
|
||||
Backend StorageBackend `yaml:"backend"`
|
||||
RootPath string `yaml:"root_path"`
|
||||
Bucket string `yaml:"bucket"`
|
||||
}
|
||||
|
||||
// ConfigFromFile loads configuration from a YAML file.
|
||||
// JWT secret key is loaded from JWT_SECRET_KEY env var (base64 encoded),
|
||||
// falling back to the file path specified in jwt.secret_key_path.
|
||||
func ConfigFromFile(path string) (*Config, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("config file not found: %s", path)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var config Config
|
||||
if err := yaml.Unmarshal(data, &config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Load JWT secret key (priority: env var > config base64 > config file path)
|
||||
if envKey := os.Getenv("JWT_SECRET_KEY"); envKey != "" {
|
||||
key, err := base64.StdEncoding.DecodeString(envKey)
|
||||
if err != nil {
|
||||
return nil, errors.New("JWT_SECRET_KEY env var is not valid base64")
|
||||
}
|
||||
config.JWT.SecretKey = key
|
||||
} else if config.JWT.SecretKeyBase64 != "" {
|
||||
key, err := base64.StdEncoding.DecodeString(config.JWT.SecretKeyBase64)
|
||||
if err != nil {
|
||||
return nil, errors.New("jwt.secret_key_base64 is not valid base64")
|
||||
}
|
||||
config.JWT.SecretKey = key
|
||||
} else if config.JWT.SecretKeyPath != "" {
|
||||
keyData, err := os.ReadFile(config.JWT.SecretKeyPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key, err := base64.StdEncoding.DecodeString(string(keyData))
|
||||
if err != nil {
|
||||
return nil, errors.New("jwt.secret_key_path file content is not valid base64")
|
||||
}
|
||||
config.JWT.SecretKey = key
|
||||
}
|
||||
|
||||
if errs := config.Validate(); len(errs) > 0 {
|
||||
return nil, NewConfigError(errs...)
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// Validate checks for required configuration fields.
|
||||
func (c *Config) Validate() []error {
|
||||
var errs []error
|
||||
|
||||
// Server
|
||||
if c.Server.Port == 0 {
|
||||
errs = append(errs, errors.New("server.port is required"))
|
||||
}
|
||||
|
||||
// Database
|
||||
if c.Database.PostgresURL == "" {
|
||||
errs = append(errs, errors.New("database.postgres_url is required"))
|
||||
}
|
||||
|
||||
// JWT
|
||||
if c.JWT.Issuer == "" {
|
||||
errs = append(errs, errors.New("jwt.issuer is required"))
|
||||
}
|
||||
if c.JWT.Audience == "" {
|
||||
errs = append(errs, errors.New("jwt.audience is required"))
|
||||
}
|
||||
if len(c.JWT.SecretKey) == 0 {
|
||||
errs = append(errs, errors.New("jwt secret key is required (set JWT_SECRET_KEY env var, jwt.secret_key_base64, or jwt.secret_key_path)"))
|
||||
}
|
||||
|
||||
// Storage
|
||||
if c.Storage.Mode == "" {
|
||||
errs = append(errs, errors.New("storage.mode is required"))
|
||||
} else if c.Storage.Mode != StorageModeFlat && c.Storage.Mode != StorageModeHierarchical {
|
||||
errs = append(errs, errors.New("storage.mode must be 'flat' or 'hierarchical'"))
|
||||
}
|
||||
|
||||
if c.Storage.Backend == "" {
|
||||
errs = append(errs, errors.New("storage.backend is required"))
|
||||
} else if c.Storage.Backend != StorageBackendFS && c.Storage.Backend != StorageBackendS3 {
|
||||
errs = append(errs, errors.New("storage.backend must be 'fs' or 's3'"))
|
||||
}
|
||||
|
||||
if c.Storage.Backend == StorageBackendFS && c.Storage.RootPath == "" {
|
||||
errs = append(errs, errors.New("storage.root_path is required when backend is 'fs'"))
|
||||
}
|
||||
if c.Storage.Backend == StorageBackendS3 {
|
||||
if c.Storage.Bucket == "" {
|
||||
errs = append(errs, errors.New("storage.bucket is required when backend is 's3'"))
|
||||
}
|
||||
if c.Storage.Mode == StorageModeHierarchical {
|
||||
errs = append(errs, errors.New("storage.mode must be 'flat' when backend is 's3'"))
|
||||
}
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
@@ -5,17 +5,17 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ServerConfigError struct {
|
||||
type ConfigError struct {
|
||||
Errors []error
|
||||
}
|
||||
|
||||
func NewServerConfigError(errs ...error) *ServerConfigError {
|
||||
return &ServerConfigError{Errors: errs}
|
||||
func NewConfigError(errs ...error) *ConfigError {
|
||||
return &ConfigError{Errors: errs}
|
||||
}
|
||||
|
||||
func (e *ServerConfigError) Error() string {
|
||||
func (e *ConfigError) Error() string {
|
||||
sb := strings.Builder{}
|
||||
sb.WriteString("invalid server config:\n")
|
||||
sb.WriteString("invalid config:\n")
|
||||
for _, err := range e.Errors {
|
||||
sb.WriteString(fmt.Sprintf(" - %s\n", err.Error()))
|
||||
}
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
package drexa
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/get-drexa/drexa/internal/auth"
|
||||
"github.com/get-drexa/drexa/internal/blob"
|
||||
@@ -16,24 +12,34 @@ import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type ServerConfig struct {
|
||||
Port int
|
||||
PostgresURL string
|
||||
JWTIssuer string
|
||||
JWTAudience string
|
||||
JWTSecretKey []byte
|
||||
}
|
||||
|
||||
func NewServer(c ServerConfig) (*fiber.App, error) {
|
||||
func NewServer(c Config) (*fiber.App, error) {
|
||||
app := fiber.New()
|
||||
db := database.NewFromPostgres(c.PostgresURL)
|
||||
db := database.NewFromPostgres(c.Database.PostgresURL)
|
||||
|
||||
// Initialize blob store based on config
|
||||
var blobStore blob.Store
|
||||
switch c.Storage.Backend {
|
||||
case StorageBackendFS:
|
||||
blobStore = blob.NewFSStore(blob.FSStoreConfig{
|
||||
Root: c.Storage.RootPath,
|
||||
})
|
||||
case StorageBackendS3:
|
||||
return nil, fmt.Errorf("s3 storage backend not yet implemented")
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown storage backend: %s", c.Storage.Backend)
|
||||
}
|
||||
|
||||
// Initialize key resolver based on config
|
||||
var keyResolver virtualfs.BlobKeyResolver
|
||||
switch c.Storage.Mode {
|
||||
case StorageModeFlat:
|
||||
keyResolver = virtualfs.NewFlatKeyResolver()
|
||||
case StorageModeHierarchical:
|
||||
keyResolver = virtualfs.NewHierarchicalKeyResolver(db)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown storage mode: %s", c.Storage.Mode)
|
||||
}
|
||||
|
||||
// TODO: load correct blob store and resolver from config
|
||||
blobStore := blob.NewFSStore(blob.FSStoreConfig{
|
||||
Root: os.Getenv("BLOB_ROOT"),
|
||||
UploadURL: os.Getenv("BLOB_UPLOAD_URL"),
|
||||
})
|
||||
keyResolver := virtualfs.NewFlatKeyResolver()
|
||||
vfs, err := virtualfs.NewVirtualFS(db, blobStore, keyResolver)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create virtual file system: %w", err)
|
||||
@@ -41,9 +47,9 @@ func NewServer(c ServerConfig) (*fiber.App, error) {
|
||||
|
||||
userService := user.NewService(db)
|
||||
authService := auth.NewService(db, userService, auth.TokenConfig{
|
||||
Issuer: c.JWTIssuer,
|
||||
Audience: c.JWTAudience,
|
||||
SecretKey: c.JWTSecretKey,
|
||||
Issuer: c.JWT.Issuer,
|
||||
Audience: c.JWT.Audience,
|
||||
SecretKey: c.JWT.SecretKey,
|
||||
})
|
||||
uploadService := upload.NewService(vfs, blobStore)
|
||||
|
||||
@@ -53,50 +59,3 @@ func NewServer(c ServerConfig) (*fiber.App, error) {
|
||||
|
||||
return app, nil
|
||||
}
|
||||
|
||||
// ServerConfigFromEnv creates a ServerConfig from environment variables.
|
||||
func ServerConfigFromEnv() (*ServerConfig, error) {
|
||||
c := ServerConfig{
|
||||
PostgresURL: os.Getenv("POSTGRES_URL"),
|
||||
JWTIssuer: os.Getenv("JWT_ISSUER"),
|
||||
JWTAudience: os.Getenv("JWT_AUDIENCE"),
|
||||
}
|
||||
|
||||
errs := []error{}
|
||||
|
||||
keyHex := os.Getenv("JWT_SECRET_KEY")
|
||||
if keyHex == "" {
|
||||
errs = append(errs, errors.New("JWT_SECRET_KEY is required"))
|
||||
} else {
|
||||
k, err := hex.DecodeString(keyHex)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("failed to decode JWT_SECRET_KEY: %w", err))
|
||||
}
|
||||
c.JWTSecretKey = k
|
||||
}
|
||||
|
||||
p, err := strconv.Atoi(os.Getenv("PORT"))
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("failed to parse PORT: %w", err))
|
||||
}
|
||||
c.Port = p
|
||||
|
||||
if c.PostgresURL == "" {
|
||||
errs = append(errs, errors.New("POSTGRES_URL is required"))
|
||||
}
|
||||
if c.JWTIssuer == "" {
|
||||
errs = append(errs, errors.New("JWT_ISSUER is required"))
|
||||
}
|
||||
if c.JWTAudience == "" {
|
||||
errs = append(errs, errors.New("JWT_AUDIENCE is required"))
|
||||
}
|
||||
if len(c.JWTSecretKey) == 0 {
|
||||
errs = append(errs, errors.New("JWT_SECRET_KEY is required"))
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return nil, NewServerConfigError(errs...)
|
||||
}
|
||||
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user