package main import ( "context" "log" "net/http" "os" "os/signal" "syscall" "time" "github.com/calendarapi/internal/api" "github.com/calendarapi/internal/api/handlers" "github.com/calendarapi/internal/auth" "github.com/calendarapi/internal/config" "github.com/calendarapi/internal/middleware" "github.com/calendarapi/internal/repository" "github.com/calendarapi/internal/scheduler" "github.com/calendarapi/internal/service" "github.com/golang-migrate/migrate/v4" _ "github.com/golang-migrate/migrate/v4/database/postgres" _ "github.com/golang-migrate/migrate/v4/source/file" "github.com/jackc/pgx/v5/pgxpool" ) func main() { cfg := config.Load() ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() pool, err := pgxpool.New(ctx, cfg.DatabaseURL) if err != nil { log.Fatalf("connect to database: %v", err) } defer pool.Close() if err := pool.Ping(ctx); err != nil { log.Fatalf("ping database: %v", err) } log.Println("connected to PostgreSQL") runMigrations(cfg.DatabaseURL) queries := repository.New(pool) jwtManager := auth.NewJWTManager(cfg.JWTSecret) var sched service.ReminderScheduler if cfg.RedisAddr != "" { realSched := scheduler.NewScheduler(cfg.RedisAddr) defer realSched.Close() sched = realSched worker := scheduler.NewReminderWorker(queries) asynqSrv := scheduler.StartWorker(cfg.RedisAddr, worker) defer asynqSrv.Shutdown() log.Println("Redis connected, background jobs enabled") } else { sched = scheduler.NoopScheduler{} log.Println("Redis not configured, background jobs disabled") } auditSvc := service.NewAuditService(queries) authSvc := service.NewAuthService(pool, queries, jwtManager, auditSvc) userSvc := service.NewUserService(pool, queries, auditSvc) calSvc := service.NewCalendarService(pool, queries, auditSvc, cfg.BaseURL) eventSvc := service.NewEventService(pool, queries, calSvc, auditSvc, sched) contactSvc := service.NewContactService(queries, auditSvc) availSvc := service.NewAvailabilityService(queries, calSvc, eventSvc) bookingSvc := service.NewBookingService(pool, queries, calSvc, eventSvc) apiKeySvc := service.NewAPIKeyService(queries) reminderSvc := service.NewReminderService(queries, calSvc, sched) attendeeSvc := service.NewAttendeeService(queries, calSvc) h := api.Handlers{ Auth: handlers.NewAuthHandler(authSvc, userSvc), User: handlers.NewUserHandler(userSvc), Calendar: handlers.NewCalendarHandler(calSvc), Sharing: handlers.NewSharingHandler(calSvc), Event: handlers.NewEventHandler(eventSvc), Reminder: handlers.NewReminderHandler(reminderSvc), Attendee: handlers.NewAttendeeHandler(attendeeSvc), Contact: handlers.NewContactHandler(contactSvc), Availability: handlers.NewAvailabilityHandler(availSvc), Booking: handlers.NewBookingHandler(bookingSvc), APIKey: handlers.NewAPIKeyHandler(apiKeySvc), ICS: handlers.NewICSHandler(calSvc, eventSvc, queries), } authMW := middleware.NewAuthMiddleware(jwtManager, queries) rateLimiter := middleware.NewRateLimiter(100, 200) router := api.NewRouter(h, authMW, rateLimiter) srv := &http.Server{ Addr: ":" + cfg.ServerPort, Handler: router, ReadTimeout: 15 * time.Second, WriteTimeout: 15 * time.Second, IdleTimeout: 60 * time.Second, } go func() { log.Printf("server starting on port %s (env=%s)", cfg.ServerPort, cfg.Env) if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("server error: %v", err) } }() quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit log.Println("shutting down server...") shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second) defer shutdownCancel() if err := srv.Shutdown(shutdownCtx); err != nil { log.Fatalf("server shutdown error: %v", err) } log.Println("server stopped") } func runMigrations(databaseURL string) { m, err := migrate.New("file://migrations", databaseURL) if err != nil { log.Printf("migration init: %v (skipping)", err) return } defer m.Close() if err := m.Up(); err != nil && err != migrate.ErrNoChange { log.Fatalf("migration error: %v", err) } v, dirty, _ := m.Version() log.Printf("migrations applied (version=%d, dirty=%v)", v, dirty) }