- Config: try ENV_FILE, .env, ../.env for loading; trim trailing slash from BaseURL - Log BASE_URL at server startup for verification - .env.example: document BASE_URL - Tasks, projects, tags, migrations and related API/handlers Made-with: Cursor
170 lines
5.7 KiB
Go
170 lines
5.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()
|
|
log.Printf("BASE_URL=%s", cfg.BaseURL)
|
|
|
|
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)
|
|
|
|
taskWebhookSvc := service.NewTaskWebhookService(queries)
|
|
taskSvc := service.NewTaskService(queries, auditSvc, taskWebhookSvc)
|
|
projectSvc := service.NewProjectService(queries, auditSvc)
|
|
tagSvc := service.NewTagService(queries)
|
|
taskDepSvc := service.NewTaskDependencyService(queries)
|
|
var taskReminderScheduler service.TaskReminderScheduler = scheduler.NoopScheduler{}
|
|
if realSched, ok := sched.(*scheduler.Scheduler); ok {
|
|
taskReminderScheduler = realSched
|
|
}
|
|
taskReminderSvc := service.NewTaskReminderService(queries, taskReminderScheduler)
|
|
|
|
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),
|
|
Task: handlers.NewTaskHandler(taskSvc),
|
|
Project: handlers.NewProjectHandler(projectSvc),
|
|
Tag: handlers.NewTagHandler(tagSvc),
|
|
TaskDependency: handlers.NewTaskDependencyHandler(taskDepSvc),
|
|
TaskReminder: handlers.NewTaskReminderHandler(taskReminderSvc),
|
|
TaskWebhook: handlers.NewTaskWebhookHandler(taskWebhookSvc),
|
|
}
|
|
|
|
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)
|
|
}
|