main.go
Rama develop · réplica de lectura del repositorio original.
package main
import (
"context"
"fmt"
"log"
"net/http"
"strings"
"time"
"donetick.com/core/config"
docs "donetick.com/core/docs"
"donetick.com/core/external/payment"
"donetick.com/core/frontend"
auth "donetick.com/core/internal/auth"
"donetick.com/core/internal/auth/apple"
"donetick.com/core/internal/bridge"
"donetick.com/core/internal/calendar"
calRepo "donetick.com/core/internal/calendar/repo"
"donetick.com/core/internal/chore"
chRepo "donetick.com/core/internal/chore/repo"
"donetick.com/core/internal/circle"
cRepo "donetick.com/core/internal/circle/repo"
"donetick.com/core/internal/database"
"donetick.com/core/internal/device"
dRepo "donetick.com/core/internal/device/repo"
"donetick.com/core/internal/email"
"donetick.com/core/internal/events"
label "donetick.com/core/internal/label"
lRepo "donetick.com/core/internal/label/repo"
"donetick.com/core/internal/mfa"
"donetick.com/core/internal/project"
pjRepo "donetick.com/core/internal/project/repo"
reward "donetick.com/core/internal/reward"
rwRepo "donetick.com/core/internal/reward/repo"
sRepo "donetick.com/core/external/payment/repo"
sService "donetick.com/core/external/payment/service"
notifier "donetick.com/core/internal/notifier"
"donetick.com/core/internal/notifier/dispatch"
"donetick.com/core/internal/notifier/push"
nRepo "donetick.com/core/internal/notifier/repo"
nps "donetick.com/core/internal/notifier/service"
"donetick.com/core/internal/notifier/service/bridgenotifier"
discord "donetick.com/core/internal/notifier/service/discord"
"donetick.com/core/internal/notifier/service/fcm"
"donetick.com/core/internal/notifier/service/pushover"
telegram "donetick.com/core/internal/notifier/service/telegram"
pRepo "donetick.com/core/internal/points/repo"
"donetick.com/core/internal/realtime"
"donetick.com/core/internal/resource"
"donetick.com/core/internal/storage"
storageRepo "donetick.com/core/internal/storage/repo"
spRepo "donetick.com/core/internal/subtask/repo"
dsync "donetick.com/core/internal/sync"
"donetick.com/core/internal/thing"
tRepo "donetick.com/core/internal/thing/repo"
"donetick.com/core/internal/user"
uRepo "donetick.com/core/internal/user/repo"
"donetick.com/core/internal/utils"
"donetick.com/core/logging"
"donetick.com/core/migrations"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
"go.uber.org/fx"
"gorm.io/gorm"
"donetick.com/core/internal/filter"
fRepo "donetick.com/core/internal/filter/repo"
)
func main() {
// Load configuration first
cfg := config.LoadConfig()
// Configure logging from application config
logging.SetConfigFromAppConfig(
cfg.Logging.Level,
cfg.Logging.Encoding,
cfg.Logging.Development,
)
app := fx.New(
// Startup runs DB migrations synchronously in an OnStart hook. On large
// datasets the backfill migrations take well over fx's 15s default,
// which would abort startup and crash-loop. Give them room to finish.
fx.StartTimeout(15*time.Minute),
fx.Supply(cfg),
fx.Supply(logging.DefaultLogger().Desugar()),
// fx.Provide(config.NewConfig),
fx.Provide(auth.NewAuthMiddleware),
fx.Provide(auth.APITokenMiddleware),
fx.Provide(auth.NewMultiAuthMiddleware),
fx.Provide(auth.NewIdentityProvider),
fx.Provide(resource.NewHandler),
// fx.Provide(NewBot),
fx.Provide(database.NewDatabase),
fx.Provide(chRepo.NewChoreRepository),
fx.Provide(chore.NewHandler),
fx.Provide(calendar.NewHandler),
fx.Provide(calRepo.NewCalendarRepository),
fx.Provide(uRepo.NewUserRepository),
fx.Provide(user.NewDeletionService),
fx.Provide(user.NewHandler),
fx.Provide(cRepo.NewCircleRepository),
fx.Provide(circle.NewHandler),
// Device management:
fx.Provide(dRepo.NewDeviceRepository),
fx.Provide(device.NewHandler),
// Donetick Bridge (hosted push-notification relay):
fx.Provide(func(cfg *config.Config) bridge.CoreConfig {
return bridge.CoreConfig{
Enabled: cfg.Bridge.Enabled,
BaseURL: cfg.Bridge.BaseURL,
InstanceID: cfg.Bridge.InstanceID,
InstanceToken: cfg.Bridge.InstanceToken,
TimeoutSeconds: cfg.Bridge.TimeoutSeconds,
}
}),
fx.Provide(bridge.NewSettingsRepository),
fx.Provide(bridge.NewService),
fx.Provide(bridge.NewHandler),
fx.Provide(nRepo.NewNotificationRepository),
fx.Provide(nps.NewNotificationPlanner),
fx.Provide(nps.NewDigestPlanner),
// add notifier
fx.Provide(pushover.NewPushover),
fx.Provide(telegram.NewTelegramNotifier),
fx.Provide(discord.NewDiscordNotifier),
fx.Provide(bridgenotifier.NewBridgeNotifier),
fx.Provide(notifier.NewNotifier),
fx.Provide(events.NewEventsProducer),
fx.Provide(fcm.NewFCMNotifier),
fx.Provide(push.NewSender),
fx.Provide(dispatch.NewDispatcher),
// Rate limiter
fx.Provide(utils.NewRateLimiter),
// add email sender (implementation selected by email.provider config):
fx.Provide(email.NewSender),
// MFA services
fx.Provide(mfa.NewService),
fx.Provide(mfa.NewCleanupService),
// Auth services
fx.Provide(auth.NewTokenService),
fx.Provide(auth.NewCleanupService),
fx.Provide(apple.NewAppleService),
// add handlers also
fx.Provide(newServer),
fx.Provide(notifier.NewScheduler),
// things
fx.Provide(tRepo.NewThingRepository),
// points
fx.Provide(pRepo.NewPointsRepository),
fx.Provide(spRepo.NewSubTasksRepository),
// Labels:
fx.Provide(lRepo.NewLabelRepository),
fx.Provide(label.NewHandler),
// Rewards:
fx.Provide(rwRepo.NewRewardRepository),
fx.Provide(reward.NewHandler),
// Projects:
fx.Provide(pjRepo.NewProjectRepository),
fx.Provide(project.NewHandler),
// Filters:
fx.Provide(fRepo.NewFilterRepository),
fx.Provide(filter.NewHandler),
fx.Provide(thing.NewAPI),
fx.Provide(thing.NewHandler),
// External Only:
fx.Provide(sService.NewStripeService,
sRepo.NewStripeDB,
sRepo.NewRevenueCatDB,
sRepo.NewSubscriptionDB,
),
fx.Provide(payment.NewHandler),
fx.Provide(payment.NewWebhook),
fx.Provide(chore.NewAPI),
// Sync:
fx.Provide(dsync.NewHandler),
// Frontend
fx.Provide(frontend.NewHandler),
// Docs
fx.Provide(docs.NewHandler),
// storage: factory selects local vs S3 based on config.Storage.StorageType.
// Set storage_type: "local" in config for local dev, leave blank for S3.
fx.Provide(storage.NewStorage),
fx.Provide(storage.NewURLSigner),
fx.Provide(storage.NewHandler),
fx.Provide(storage.NewDraftCleanupService),
fx.Provide(storageRepo.NewStorageRepository),
// backup service
// fx.Provide(backup.NewService),
// fx.Provide(backup.NewHandler),
// Real-time service and components
fx.Provide(realtime.NewRealTimeService),
fx.Provide(realtime.NewTicketStore),
fx.Provide(realtime.NewAuthMiddleware),
fx.Provide(realtime.NewPollingHandler),
// MCP server
// fx.Invoke(RunApp),
fx.Invoke(
chore.Routes,
calendar.Routes,
chore.APIs,
user.Routes,
circle.Routes,
device.Routes,
bridge.Routes,
thing.Routes,
thing.APIs,
label.Routes,
reward.Routes,
project.Routes,
filter.Routes,
storage.Routes,
frontend.Routes,
docs.Routes,
resource.Routes,
// backup.Routes,
payment.Routes,
payment.Webhooks,
dsync.Routes,
realtime.Routes, // (router, rtAuthMiddleware, pollingHandler, jwtAuth)
func(r *gin.Engine) {},
),
)
if err := app.Err(); err != nil {
log.Fatal(err)
}
app.Run()
}
func newServer(lc fx.Lifecycle, cfg *config.Config, db *gorm.DB, notifier *notifier.Scheduler, eventProducer *events.EventsProducer, mfaCleanup *mfa.CleanupService, authCleanup *auth.CleanupService, rts *realtime.RealTimeService, draftCleanup *storage.DraftCleanupService, ticketStore *realtime.TicketStore) *gin.Engine {
// Set Gin mode based on logging configuration
if cfg.Logging.Development || strings.ToLower(cfg.Logging.Level) == "debug" {
gin.SetMode(gin.DebugMode)
} else {
gin.SetMode(gin.ReleaseMode)
}
// log when http request is made:
r := gin.New()
srv := &http.Server{
Addr: fmt.Sprintf(":%d", cfg.Server.Port),
Handler: r,
ReadTimeout: cfg.Server.ReadTimeout,
WriteTimeout: cfg.Server.WriteTimeout,
}
corsConfig := cors.DefaultConfig()
allowOrigins := make(map[string]bool)
for _, origin := range cfg.Server.CorsAllowOrigins {
allowOrigins[origin] = true
}
corsConfig.AllowOriginFunc = func(origin string) bool {
return allowOrigins[origin]
}
corsConfig.AllowCredentials = true
// Add all headers that browsers commonly send
corsConfig.AddAllowHeaders(
"Authorization",
"secretkey",
"Cache-Control",
"Content-Type",
"Accept",
"Sec-Ch-Ua",
"Sec-Ch-Ua-Mobile",
"Sec-Ch-Ua-Platform",
"User-Agent",
"Referer",
"X-Impersonate-User-ID",
"refresh_token",
)
// Expose headers that the frontend might need
corsConfig.AddExposeHeaders("Content-Type")
r.Use(cors.New(corsConfig))
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
if cfg.Database.Migration {
database.Migration(db)
migrations.Run(context.Background(), db)
err := database.MigrationScripts(db, cfg)
if err != nil {
panic(err)
}
}
notifier.Start(context.Background())
eventProducer.Start(context.Background())
mfaCleanup.Start(context.Background())
authCleanup.Start(context.Background())
draftCleanup.Start(context.Background())
// Start real-time service
if err := rts.Start(ctx); err != nil {
log.Printf("Failed to start real-time service: %v", err)
}
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %s\n", err)
}
}()
return nil
},
OnStop: func(ctx context.Context) error {
// Stop real-time service first with timeout
done := make(chan error, 1)
go func() {
done <- rts.Stop()
}()
select {
case err := <-done:
if err != nil {
log.Printf("Failed to stop real-time service: %v", err)
}
case <-time.After(5 * time.Second):
log.Printf("Real-time service shutdown timeout, forcing shutdown")
}
mfaCleanup.Stop()
authCleanup.Stop()
draftCleanup.Stop()
// Stop the SSE ticket store cleanup goroutine
ticketStore.Stop()
// Shutdown HTTP server with timeout
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Printf("Server shutdown timeout: %s", err)
// Force close
srv.Close()
}
return nil
},
})
// Simple health-check endpoint
r.GET("/api/v1/health", func(c *gin.Context) {
c.Status(http.StatusOK) // 200 - OK if server started
})
return r
}