Files
CalendarApi/cmd/server/main.go
Michilis 75105b8b46 Add OpenAPI docs, frontend, migrations, and API updates
- OpenAPI: add missing endpoints (add-from-url, subscriptions, public availability)
- OpenAPI: CalendarSubscription schema, Subscriptions tag
- Frontend app
- Migrations: count_for_availability, subscriptions_sync, user_preferences, calendar_settings
- Config, rate limit, auth, calendar, booking, ICS, availability, user service updates

Made-with: Cursor
2026-03-02 14:07:55 +00:00

152 lines
4.7 KiB
Go

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)
auditSvc := service.NewAuditService(queries)
calSvc := service.NewCalendarService(pool, queries, auditSvc, cfg.BaseURL)
var sched service.ReminderScheduler
if cfg.RedisAddr != "" {
sched = scheduler.NewScheduler(cfg.RedisAddr)
} else {
sched = scheduler.NoopScheduler{}
}
if realSched, ok := sched.(*scheduler.Scheduler); ok {
defer realSched.Close()
}
eventSvc := service.NewEventService(pool, queries, calSvc, auditSvc, sched)
if cfg.RedisAddr != "" {
realSched := sched.(*scheduler.Scheduler)
icsHandler := handlers.NewICSHandler(calSvc, eventSvc, queries)
subSyncWorker := scheduler.NewSubscriptionSyncWorker(icsHandler)
worker := scheduler.NewReminderWorker(queries)
asynqSrv := scheduler.StartWorker(cfg.RedisAddr, worker, subSyncWorker)
defer asynqSrv.Shutdown()
enqueuerCtx, enqueuerCancel := context.WithCancel(context.Background())
defer enqueuerCancel()
go scheduler.StartSubscriptionEnqueuer(enqueuerCtx, queries, realSched, 15*time.Minute)
log.Println("Redis connected, background jobs enabled")
} else {
log.Println("Redis not configured, background jobs disabled")
}
authSvc := service.NewAuthService(pool, queries, jwtManager, auditSvc)
userSvc := service.NewUserService(pool, queries, auditSvc)
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(cfg.RateLimitRPS, cfg.RateLimitBurst)
router := api.NewRouter(h, authMW, rateLimiter, cfg)
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)
}