package main import ( "log" "os" "path/filepath" "asset-tracker/internal/api" "asset-tracker/internal/auth" "asset-tracker/internal/config" "asset-tracker/internal/model" "asset-tracker/internal/scheduler" "github.com/gin-gonic/gin" "golang.org/x/crypto/bcrypt" "gorm.io/driver/sqlite" "gorm.io/gorm" ) func main() { cfg := config.Load() if cfg.AppEnv == "production" && cfg.JWTSecret == "change_me_in_production" { log.Fatal("JWT_SECRET must be set in production") } if err := os.MkdirAll(filepath.Dir(cfg.DBPath), 0o755); err != nil { log.Fatalf("create db dir failed: %v", err) } db, err := gorm.Open(sqlite.Open(cfg.DBPath), &gorm.Config{}) if err != nil { log.Fatalf("open db failed: %v", err) } if err := db.AutoMigrate(&model.User{}, &model.Category{}, &model.Asset{}, &model.Reminder{}, &model.AuditLog{}, &model.RefreshSession{}, &model.ReminderDeadLetter{}); err != nil { log.Fatalf("auto migrate failed: %v", err) } if err := ensureDefaultUser(db, cfg.DefaultUsername, cfg.DefaultPassword, cfg.DefaultTimezone); err != nil { log.Fatalf("ensure default user failed: %v", err) } scheduler.StartReminderScan(db) tm := auth.NewTokenManager(cfg.JWTSecret, cfg.AccessTTLMinutes, cfg.RefreshTTLHours) h := api.NewHandler(db, tm) r := gin.New() r.Use(gin.Recovery()) r.Use(api.RequestID()) r.Use(api.AccessLog()) api.RegisterRoutes(r, h, tm) log.Printf("asset-tracker listening on %s", cfg.HTTPAddr) if err := r.Run(cfg.HTTPAddr); err != nil { log.Fatalf("run http server failed: %v", err) } } func ensureDefaultUser(db *gorm.DB, username, password, timezone string) error { var cnt int64 if err := db.Model(&model.User{}).Where("username = ?", username).Count(&cnt).Error; err != nil { return err } if cnt > 0 { return nil } hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { return err } u := model.User{ Username: username, PasswordHash: string(hash), Timezone: timezone, } return db.Create(&u).Error }