feat: initial blog engine with multi-language, profile, and role system
- Gin + GORM + Tailwind CSS blog engine - OS-aware config (Linux /etc/blog_go/, Windows ./win/etc/blog_go/) - SQLite by default, MySQL support via config - Auto-migrate database + seed admin user on first run - Session-based auth (login/logout) - i18n: Chinese/English with auto-detect + manual switch - Avatar dropdown nav with click-to-toggle menu - Profile page: avatar upload, edit info, change password - Role system: admin (full access) and author (profile only) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Config holds all application configuration.
|
||||
type Config struct {
|
||||
Database DatabaseConfig `yaml:"database"`
|
||||
Port string `yaml:"port"`
|
||||
Path string `yaml:"path"`
|
||||
Secret string `yaml:"secret"`
|
||||
}
|
||||
|
||||
// DatabaseConfig holds database-specific configuration.
|
||||
type DatabaseConfig struct {
|
||||
Type string `yaml:"type"` // "sqlite" (default) or "mysql"
|
||||
DSN string `yaml:"dsn"` // MySQL connection string (ignored for sqlite)
|
||||
}
|
||||
|
||||
// defaultPort is the fallback web server port.
|
||||
const defaultPort = "8080"
|
||||
|
||||
// getConfigPath returns the OS-aware config directory and config file path.
|
||||
func getConfigPath() (dir, file string) {
|
||||
if runtime.GOOS == "windows" {
|
||||
dir = filepath.Join(".", "win", "etc", "blog_go")
|
||||
} else {
|
||||
dir = filepath.Join("/", "etc", "blog_go")
|
||||
}
|
||||
file = filepath.Join(dir, "config.yaml")
|
||||
return
|
||||
}
|
||||
|
||||
// getDefaultStoragePath returns the OS-aware default storage path.
|
||||
func getDefaultStoragePath() string {
|
||||
if runtime.GOOS == "windows" {
|
||||
return filepath.Join(".", "win", "srv", "blog_go")
|
||||
}
|
||||
return filepath.Join("/", "srv", "blog_go")
|
||||
}
|
||||
|
||||
// generateSecret returns a random-ish hex string for the session secret.
|
||||
func generateSecret() string {
|
||||
// Use a combination of hostname + random to produce a unique secret.
|
||||
hostname, _ := os.Hostname()
|
||||
input := fmt.Sprintf("%s-%d", hostname, os.Getpid())
|
||||
hash := sha256.Sum256([]byte(input))
|
||||
return fmt.Sprintf("%x", hash)
|
||||
}
|
||||
|
||||
// LoadConfig reads the config file or creates one with defaults.
|
||||
func LoadConfig() *Config {
|
||||
configDir, configFile := getConfigPath()
|
||||
defaultPath := getDefaultStoragePath()
|
||||
|
||||
cfg := &Config{}
|
||||
|
||||
// Check if config file exists; create with defaults if not.
|
||||
if _, err := os.Stat(configFile); os.IsNotExist(err) {
|
||||
log.Printf("Config file not found at %s, creating with defaults...", configFile)
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
log.Fatalf("Failed to create config directory %s: %v", configDir, err)
|
||||
}
|
||||
|
||||
cfg = &Config{
|
||||
Database: DatabaseConfig{
|
||||
Type: "sqlite",
|
||||
DSN: "",
|
||||
},
|
||||
Port: defaultPort,
|
||||
Path: defaultPath,
|
||||
Secret: generateSecret(),
|
||||
}
|
||||
|
||||
data, err := yaml.Marshal(cfg)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to marshal default config: %v", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(configFile, data, 0644); err != nil {
|
||||
log.Fatalf("Failed to write config file %s: %v", configFile, err)
|
||||
}
|
||||
log.Printf("Default config created at %s", configFile)
|
||||
return cfg
|
||||
}
|
||||
|
||||
// Read existing config file.
|
||||
data, err := os.ReadFile(configFile)
|
||||
if err != nil {
|
||||
log.Printf("Warning: could not read config file %s: %v, using defaults", configFile, err)
|
||||
return applyDefaults(cfg, defaultPath)
|
||||
}
|
||||
|
||||
if err := yaml.Unmarshal(data, cfg); err != nil {
|
||||
log.Printf("Warning: malformed config file %s: %v, using defaults", configFile, err)
|
||||
cfg = &Config{}
|
||||
}
|
||||
|
||||
return applyDefaults(cfg, defaultPath)
|
||||
}
|
||||
|
||||
// applyDefaults fills zero-value fields with sensible defaults.
|
||||
func applyDefaults(cfg *Config, defaultPath string) *Config {
|
||||
if cfg.Port == "" {
|
||||
cfg.Port = defaultPort
|
||||
}
|
||||
if cfg.Database.Type == "" {
|
||||
cfg.Database.Type = "sqlite"
|
||||
}
|
||||
if cfg.Path == "" {
|
||||
cfg.Path = defaultPath
|
||||
}
|
||||
if cfg.Secret == "" {
|
||||
cfg.Secret = generateSecret()
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
Reference in New Issue
Block a user