package main import ( "context" "html/template" "io/fs" "log" "net/http" "os" "os/signal" "syscall" "time" "shop/internal/auth" "shop/internal/check" "shop/internal/config" "shop/internal/database" "shop/internal/handlers" "shop/internal/repository" "shop/internal/version" "shop/internal/web" ) func main() { cfg, err := config.Load() if err != nil { log.Fatalf("config: %v", err) } ctx := context.Background() pool, err := database.Connect(ctx, cfg.DatabaseURL) if err != nil { log.Fatalf("database: %v", err) } defer pool.Close() startupReport, err := check.WithDatabase(ctx, pool) if err != nil { log.Fatalf("version check: %v", err) } for _, it := range startupReport.Items { if it.Name == "postgresql" && it.Status == check.StatusWarn { log.Printf("warning: %s — %s", it.Name, it.Detail) } } log.Printf("ShopNova %s | Go %s | PostgreSQL check OK", version.AppVersion, version.GoRuntime()) tmpl, err := loadTemplates() if err != nil { log.Fatalf("templates: %v", err) } users := repository.NewUserRepository(pool) sessions := repository.NewSessionRepository(pool) authSvc := auth.NewService(users, sessions, cfg.SessionTTL, cfg.CookieSecure) pages := handlers.NewPages(tmpl, authSvc) products := repository.NewProductRepository(pool) home := handlers.NewHomeHandler(products, pages) health := handlers.NewHealthHandler(pool) authH := handlers.NewAuthHandler(pages, authSvc) account := handlers.NewAccountHandler(pages, authSvc) staticSub, err := fs.Sub(web.StaticFS, "static") if err != nil { log.Fatalf("static fs: %v", err) } mux := http.NewServeMux() mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticSub)))) mux.HandleFunc("GET /health", health.Health) mux.HandleFunc("GET /version", health.Version) mux.Handle("GET /", home) mux.HandleFunc("/register", authH.Register) mux.HandleFunc("/login", authH.Login) mux.HandleFunc("POST /logout", authH.Logout) mux.HandleFunc("/account", account.Account) srv := &http.Server{ Addr: cfg.HTTPAddr, Handler: mux, ReadTimeout: cfg.ReadTimeout, WriteTimeout: cfg.WriteTimeout, } go func() { log.Printf("server listening on %s", cfg.HTTPAddr) if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("listen: %v", err) } }() quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := srv.Shutdown(shutdownCtx); err != nil { log.Printf("shutdown: %v", err) } } func loadTemplates() (*template.Template, error) { funcMap := template.FuncMap{ "formatPrice": func(v float64) string { return formatRub(v) }, } return template.New("").Funcs(funcMap).ParseFS(web.TemplatesFS, "templates/*.html") } func formatRub(v float64) string { intPart := int64(v) frac := int64((v - float64(intPart)) * 100) if frac < 0 { frac = -frac } return formatThousands(intPart) + "," + pad2(frac) + " ₽" } func formatThousands(n int64) string { if n < 0 { n = -n } s := "" for n >= 1000 { s = "," + pad3(n%1000) + s n /= 1000 } return itoa(n) + s } func pad3(n int64) string { if n < 10 { return "00" + itoa(n) } if n < 100 { return "0" + itoa(n) } return itoa(n) } func pad2(n int64) string { if n < 10 { return "0" + itoa(n) } return itoa(n) } func itoa(n int64) string { if n == 0 { return "0" } var b [20]byte i := len(b) for n > 0 { i-- b[i] = byte('0' + n%10) n /= 10 } return string(b[i:]) }