package main import ( "context" "errors" "fmt" "log/slog" "os" "strings" "time" "github.com/gofiber/fiber/v2" "gorm.io/gorm" "wucher/internal/app/api" queueapp "wucher/internal/app/queueing" samlauth "wucher/internal/auth" "wucher/internal/config" filemanager "wucher/internal/domain/file_manager" filemanagerrealtime "wucher/internal/realtime/filemanager" helicopterfilerealtime "wucher/internal/realtime/helicopterfile" "wucher/internal/repository/mysql" s3repo "wucher/internal/repository/s3" "wucher/internal/resilience" "wucher/internal/service" pkglogger "wucher/internal/shared/pkg/logger" tzpkg "wucher/internal/shared/pkg/timezone" userctx "wucher/internal/shared/pkg/userctx" "wucher/internal/transport/http/handlers" ) type Repositories struct { User *mysql.UserRepository Contact *mysql.ContactRepository Role *mysql.RoleRepository Helicopter *mysql.HelicopterRepository HelicopterUsage *mysql.HelicopterUsageRepository FlightInspection *mysql.FlightInspectionRepository Flight *mysql.FlightRepository FlightData *mysql.FlightDataRepository Mission *mysql.MissionRepository ReserveAc *mysql.ReserveAcRepository HelicopterFile *mysql.HelicopterFileRepository FlightInspectionFileChecklist *mysql.FlightInspectionFileChecklistRepository BeforeFlightInspection *mysql.BeforeFlightInspectionRepository AfterFlightInspection *mysql.AfterFlightInspectionRepository FMReport *mysql.FMReportRepository FlightPrepCheck *mysql.FlightPrepCheckRepository Facility *mysql.FacilityRepository Base *mysql.BaseRepository Medicine *mysql.MedicineRepository Vocation *mysql.VocationRepository Opc *mysql.OpcRepository ForcesPresent *mysql.ForcesPresentRepository Hospital *mysql.HospitalRepository HealthInsuranceCompanies *mysql.HealthInsuranceCompaniesRepository FederalState *mysql.FederalStateRepository ICAO *mysql.ICAORepository Land *mysql.LandRepository Complaint *mysql.ComplaintRepository EASARelease *mysql.EASAReleaseRepository ActionSignoff *mysql.ActionSignoffRepository OtherPerson *mysql.OtherPersonRepository MCF *mysql.MCFRepository FleetHistory *mysql.FleetHistoryRepository HEMSOperationalData *mysql.HEMSOperationalDataRepository HEMSOperationCategory *mysql.HEMSOperationCategoryRepository HEMSOperation *mysql.HEMSOperationRepository MasterSettings *mysql.MasterSettingsRepository FileManagerFolder *mysql.FileManagerFolderRepository FileManagerFile *mysql.FileManagerFileRepository FileManagerAttachment *mysql.FileManagerAttachmentRepository DutyRoster *mysql.DutyRosterRepository AirRescuerChecklist *mysql.AirRescuerChecklistRepository InsurancePatientData *mysql.InsurancePatientDataRepository PatientData *mysql.PatientDataRepository DUL *mysql.DULRepository FleetStatus *mysql.FleetStatusRepository TokenStore *mysql.TokenStore Auth *mysql.AuthRepository Audit *mysql.AuditRepository } type Infrastructure struct { DB *gorm.DB Logger *slog.Logger SQSBreaker *resilience.Executor FileStorage filemanager.ObjectStorage EmailQueue service.EmailQueue FileQueue filemanager.FileProcessingQueue FileStatusHub *filemanagerrealtime.Hub HelicopterFileHub *helicopterfilerealtime.Hub FileTrashPolicy config.FileManagerTrashPolicyConfig FileLifecycle config.FileManagerLifecycleConfig } type Services struct { User *service.UserService Contact *service.ContactService Role *service.RoleService Helicopter *service.HelicopterService HelicopterUsage *service.HelicopterUsageService FlightInspection *service.FlightInspectionService Flight *service.FlightService FlightData *service.FlightDataService Mission *service.MissionService ReserveAc *service.ReserveAcService Takeover *service.TakeoverService OtherPerson *service.OtherPersonService HelicopterFile *service.HelicopterFileService FlightInspectionChecklist *service.FlightInspectionFileChecklistService BeforeFlightInspection *service.BeforeFlightInspectionService AfterFlightInspection *service.AfterFlightInspectionService FMReport *service.FMReportService FlightPrepCheck *service.FlightPrepCheckService Facility *service.FacilityService Base *service.BaseService Medicine *service.MedicineService Vocation *service.VocationService Opc *service.OpcService ForcesPresent *service.ForcesPresentService Hospital *service.HospitalService HealthInsuranceCompanies *service.HealthInsuranceCompaniesService FederalState *service.FederalStateService ICAO *service.ICAOService Land *service.LandService Complaint *service.ComplaintService EASARelease *service.EASAReleaseService ActionSignoff *service.ActionSignoffService MCF *service.MCFService FleetHistory *service.FleetHistoryService HEMSOperationalData *service.HEMSOperationalDataService HEMSOperationCategory *service.HEMSOperationCategoryService HEMSOperation *service.HEMSOperationService MasterSettings *service.MasterSettingsService FileManager *service.FileManagerService DutyRoster *service.DutyRosterService AirRescuerChecklist *service.AirRescuerChecklistService InsurancePatientData *service.InsurancePatientDataService PatientData *service.PatientDataService DUL *service.DULService FleetStatus *service.FleetStatusService WOPI *service.WOPIService Auth *service.AuthService Audit *service.AuditLogService } type Handlers struct { User *handlers.UserHandler Contact *handlers.ContactHandler Health *handlers.HealthHandler BuildInfo *handlers.BuildInfoHandler Auth *handlers.AuthHandler Role *handlers.RoleHandler Helicopter *handlers.HelicopterHandler HelicopterUsage *handlers.HelicopterUsageHandler HelicopterFile *handlers.HelicopterFileHandler Flight *handlers.FlightHandler FlightData *handlers.FlightDataHandler Mission *handlers.MissionHandler FMReport *handlers.FMReportHandler ReserveAc *handlers.ReserveAcHandler Takeover *handlers.TakeoverHandler OtherPerson *handlers.OtherPersonHandler Facility *handlers.FacilityHandler Base *handlers.BaseHandler Medicine *handlers.MedicineHandler Vocation *handlers.VocationHandler Opc *handlers.OpcHandler ForcesPresent *handlers.ForcesPresentHandler Hospital *handlers.HospitalHandler HealthInsuranceCompanies *handlers.HealthInsuranceCompaniesHandler FederalState *handlers.FederalStateHandler ICAO *handlers.ICAOHandler Land *handlers.LandHandler MasterSettings *handlers.MasterSettingsHandler Branding *handlers.BrandingHandler FileManagerFolder *handlers.FileManagerFolderHandler FileManagerFile *handlers.FileManagerFileHandler FileManagerAttachment *handlers.FileManagerAttachmentHandler FileManagerEvents *handlers.FileManagerEventsHandler HelicopterFileEvents *handlers.HelicopterFileEventsHandler DutyRoster *handlers.DutyRosterHandler AirRescuerChecklist *handlers.AirRescuerChecklistHandler InsurancePatientData *handlers.InsurancePatientDataHandler PatientData *handlers.PatientDataHandler DUL *handlers.DULHandler FleetStatus *handlers.FleetStatusHandler HEMSOperationalData *handlers.HEMSOperationalDataHandler HEMSOperationCategory *handlers.HEMSOperationCategoryHandler HEMSOperation *handlers.HEMSOperationHandler Complaint *handlers.ComplaintHandler EASARelease *handlers.EASAReleaseHandler ActionSignoff *handlers.ActionSignoffHandler MCF *handlers.MCFHandler } func initializeDatabase(mysqlCfg config.MySQLConfig) (*gorm.DB, error) { return mysql.Connect(mysqlCfg) } func bootstrapData(db *gorm.DB) error { if err := mysql.AutoMigrate(db); err != nil { return fmt.Errorf("auto migrate: %w", err) } if err := db.Exec("UPDATE users SET timezone = ? WHERE timezone IS NULL OR timezone = ''", tzpkg.Default).Error; err != nil { return fmt.Errorf("seed users timezone default: %w", err) } if err := api.SeedRoles(db); err != nil { return fmt.Errorf("seed roles: %w", err) } if err := api.SeedPermissions(db); err != nil { return fmt.Errorf("seed permissions: %w", err) } if err := api.SeedRolePermissions(db); err != nil { return fmt.Errorf("seed role permissions: %w", err) } if err := api.SeedBaseCategories(db); err != nil { return fmt.Errorf("seed base categories: %w", err) } if strings.EqualFold(strings.TrimSpace(os.Getenv("SEED_ROSTER_USERS")), "true") { if err := api.SeedRosterUsers(db); err != nil { return fmt.Errorf("seed roster users: %w", err) } } return nil } func initializeRepositories(db *gorm.DB) (*Repositories, error) { if db == nil { return nil, errors.New("database is required") } repos := &Repositories{ User: mysql.NewUserRepository(db), Contact: mysql.NewContactRepository(db), Role: mysql.NewRoleRepository(db), Helicopter: mysql.NewHelicopterRepository(db), HelicopterUsage: mysql.NewHelicopterUsageRepository(db), FlightInspection: mysql.NewFlightInspectionRepository(db), Flight: mysql.NewFlightRepository(db), FlightData: mysql.NewFlightDataRepository(db), Mission: mysql.NewMissionRepository(db), ReserveAc: mysql.NewReserveAcRepository(db), HelicopterFile: mysql.NewHelicopterFileRepository(db), FlightInspectionFileChecklist: mysql.NewFlightInspectionFileChecklistRepository(db), BeforeFlightInspection: mysql.NewBeforeFlightInspectionRepository(db), AfterFlightInspection: mysql.NewAfterFlightInspectionRepository(db), FMReport: mysql.NewFMReportRepository(db), FlightPrepCheck: mysql.NewFlightPrepCheckRepository(db), Facility: mysql.NewFacilityRepository(db), Base: mysql.NewBaseRepository(db), Medicine: mysql.NewMedicineRepository(db), Vocation: mysql.NewVocationRepository(db), Opc: mysql.NewOpcRepository(db), ForcesPresent: mysql.NewForcesPresentRepository(db), Hospital: mysql.NewHospitalRepository(db), HealthInsuranceCompanies: mysql.NewHealthInsuranceCompaniesRepository(db), FederalState: mysql.NewFederalStateRepository(db), ICAO: mysql.NewICAORepository(db), Land: mysql.NewLandRepository(db), Complaint: mysql.NewComplaintRepository(db), EASARelease: mysql.NewEASAReleaseRepository(db), ActionSignoff: mysql.NewActionSignoffRepository(db), OtherPerson: mysql.NewOtherPersonRepository(db), MCF: mysql.NewMCFRepository(db), FleetHistory: mysql.NewFleetHistoryRepository(db), HEMSOperationalData: mysql.NewHEMSOperationalDataRepository(db), HEMSOperationCategory: mysql.NewHEMSOperationCategoryRepository(db), HEMSOperation: mysql.NewHEMSOperationRepository(db), MasterSettings: mysql.NewMasterSettingsRepository(db), FileManagerFolder: mysql.NewFileManagerFolderRepository(db), FileManagerFile: mysql.NewFileManagerFileRepository(db), FileManagerAttachment: mysql.NewFileManagerAttachmentRepository(db), DutyRoster: mysql.NewDutyRosterRepository(db), AirRescuerChecklist: mysql.NewAirRescuerChecklistRepository(db), InsurancePatientData: mysql.NewInsurancePatientDataRepository(db), PatientData: mysql.NewPatientDataRepository(db), DUL: mysql.NewDULRepository(db), FleetStatus: mysql.NewFleetStatusRepository(db), TokenStore: mysql.NewTokenStore(db), Auth: mysql.NewAuthRepository(db), Audit: mysql.NewAuditRepository(db), } return repos, nil } func initializeInfrastructure(ctx context.Context, appCfg config.AppConfig, db *gorm.DB) (*Infrastructure, error) { appLogger := pkglogger.New(appCfg.Logging).With("component", "api") slog.SetDefault(appLogger) if err := config.ValidateDistinctQueueURLs(appCfg.SQS, appCfg.FileSQS); err != nil { return nil, err } resCfg := appCfg.Resilience sqsBreaker := resilience.NewExecutor("sqs", resCfg.SQS, appLogger, resilience.ClassifySQSError) var emailQueue service.EmailQueue if !appCfg.Queue.OutboxEnabled { emailProducer, err := queueapp.NewEmailProducer(ctx, appCfg.Queue, appCfg.SQS, sqsBreaker, appLogger) if err != nil { return nil, err } emailQueue = service.AdaptEmailQueue(emailProducer) } var fileStorage filemanager.ObjectStorage if appCfg.FileStorage.Bucket != "" && appCfg.FileStorage.Region != "" { storage, err := s3repo.NewFileObjectStorage(ctx, appCfg.FileStorage) if err != nil { return nil, err } fileStorage = storage } var fileQueue filemanager.FileProcessingQueue if !appCfg.FileQueue.OutboxEnabled && strings.TrimSpace(appCfg.FileSQS.QueueURL) != "" { fileProducer, err := queueapp.NewFileProcessingProducer(ctx, appCfg.FileQueue, appCfg.FileSQS, sqsBreaker, appLogger) if err != nil { return nil, err } fileQueue = service.AdaptFileProcessingQueue(fileProducer) } return &Infrastructure{ DB: db, Logger: appLogger, SQSBreaker: sqsBreaker, FileStorage: fileStorage, EmailQueue: emailQueue, FileQueue: fileQueue, FileStatusHub: filemanagerrealtime.NewHub(64, appLogger), HelicopterFileHub: helicopterfilerealtime.NewHub(64, appLogger), FileTrashPolicy: config.LoadFileManagerTrashPolicyConfigFromEnv(), FileLifecycle: config.LoadFileManagerLifecycleConfigFromEnv(), }, nil } func initializeServices(appCfg config.AppConfig, repos *Repositories, infra *Infrastructure) (*Services, error) { if repos == nil { return nil, errors.New("repositories are required") } if infra == nil { return nil, errors.New("infrastructure is required") } var protector service.SecretProtector if p, err := service.NewAESGCMProtectorFromBase64(appCfg.Auth.TOTPSecretKeyB64); err == nil { protector = p } fileManagerDeps := service.FileManagerServiceDependencies{ Storage: infra.FileStorage, FileQueue: infra.FileQueue, UploadPolicy: filemanager.FileUploadPolicy{}, TrashRetention: infra.FileTrashPolicy.Retention, FileStatusEvents: repos.FileManagerFile, Logger: infra.Logger, EnableS3Tagging: infra.FileLifecycle.EnableS3Tagging, } if appCfg.FileQueue.OutboxEnabled { fileManagerDeps.FileOutbox = repos.FileManagerFile fileManagerDeps.FileOutboxSerializer = queueapp.NewFileProcessingSerializer(appCfg.FileQueue, appCfg.FileSQS) } fileManagerDeps.UploadPolicy = filemanager.FileUploadPolicy{ MaxFileSizeBytes: appCfg.FileUpload.MaxFileSizeBytes, AllowedMimeTypes: appCfg.FileUpload.AllowedMimeTypes, AllowedExtensions: appCfg.FileUpload.AllowedExtensions, AllowEmptyFile: appCfg.FileUpload.AllowEmptyFile, MaxFilenameLength: appCfg.FileUpload.MaxFilenameLength, } flightDataSvc := service.NewFlightDataService(repos.FlightData) flightSvc := service.NewFlightService(repos.Flight) fileManagerSvc := service.NewFileManagerService(repos.FileManagerFolder, repos.FileManagerFile, repos.FileManagerAttachment, fileManagerDeps) helicopterSvc := service.NewHelicopterService(repos.Helicopter) helicopterUsageSvc := service.NewHelicopterUsageService(repos.HelicopterUsage) afterFlightInspectionSvc := service.NewAfterFlightInspectionService(repos.AfterFlightInspection) services := &Services{ User: service.NewUserService(repos.User), Role: service.NewRoleService(repos.Role), Helicopter: helicopterSvc, HelicopterUsage: helicopterUsageSvc, FlightInspection: service.NewFlightInspectionService(repos.FlightInspection), Flight: flightSvc, FlightData: flightDataSvc, Mission: nil, ReserveAc: service.NewReserveAcService(repos.ReserveAc), Takeover: service.NewTakeoverService(infra.DB), HelicopterFile: service.NewHelicopterFileService(repos.HelicopterFile), FlightInspectionChecklist: service.NewFlightInspectionFileChecklistService(repos.FlightInspectionFileChecklist), BeforeFlightInspection: service.NewBeforeFlightInspectionService(repos.BeforeFlightInspection), AfterFlightInspection: afterFlightInspectionSvc, FMReport: service.NewFMReportService(repos.FMReport), FlightPrepCheck: service.NewFlightPrepCheckService(repos.FlightPrepCheck, repos.FlightInspection), Facility: service.NewFacilityService(repos.Facility), Base: service.NewBaseService(repos.Base), Medicine: service.NewMedicineService(repos.Medicine), Vocation: service.NewVocationService(repos.Vocation), Opc: service.NewOpcService(repos.Opc), ForcesPresent: service.NewForcesPresentService(repos.ForcesPresent), Hospital: service.NewHospitalService(repos.Hospital), HealthInsuranceCompanies: service.NewHealthInsuranceCompaniesService(repos.HealthInsuranceCompanies), FederalState: service.NewFederalStateService(repos.FederalState), ICAO: service.NewICAOService(repos.ICAO), Land: service.NewLandService(repos.Land), Complaint: service.NewComplaintService(repos.Complaint), EASARelease: service.NewEASAReleaseService(repos.EASARelease), ActionSignoff: service.NewActionSignoffService(repos.ActionSignoff), OtherPerson: service.NewOtherPersonService(repos.OtherPerson), MCF: service.NewMCFService(repos.MCF), FleetHistory: service.NewFleetHistoryService(repos.FleetHistory), HEMSOperationalData: service.NewHEMSOperationalDataService(repos.HEMSOperationalData), HEMSOperationCategory: service.NewHEMSOperationCategoryService(repos.HEMSOperationCategory), HEMSOperation: service.NewHEMSOperationService(repos.HEMSOperation), MasterSettings: service.NewMasterSettingsService(repos.MasterSettings, service.MasterSettingsServiceDependencies{Protector: protector}), FileManager: fileManagerSvc, DutyRoster: service.NewDutyRosterService(repos.DutyRoster), AirRescuerChecklist: service.NewAirRescuerChecklistService(repos.AirRescuerChecklist), InsurancePatientData: service.NewInsurancePatientDataService(repos.InsurancePatientData), PatientData: service.NewPatientDataService(repos.PatientData), DUL: service.NewDULService(repos.DUL), Audit: service.NewAuditLogService(repos.Audit, service.AuditLogServiceDependencies{QueueSize: appCfg.Audit.QueueSize, Workers: appCfg.Audit.Workers, IPEncryptionKey: appCfg.Auth.IPEncryptionKey}), WOPI: service.NewWOPIService(service.WOPIConfig{ TokenSecret: appCfg.WOPI.TokenSecret, ProofSecret: appCfg.WOPI.ProofSecret, DiscoveryURL: appCfg.WOPI.DiscoveryURL, PublicBaseURL: appCfg.WOPI.PublicBaseURL, EditActionURL: appCfg.WOPI.EditActionURL, TokenTTL: appCfg.WOPI.TokenTTL, MaxBodyBytes: appCfg.WOPI.MaxBodyBytes, RequireProof: appCfg.WOPI.RequireProof, }), } services.Mission = service.NewMissionService(repos.Mission, flightSvc, flightDataSvc, afterFlightInspectionSvc) services.FleetStatus = service.NewFleetStatusService(repos.FleetStatus, fileManagerSvc, helicopterSvc, services.Complaint).WithHistory(services.FleetHistory).WithUsage(services.HelicopterUsage) flightDataSvc.WithAOGRecalculator(services.FleetStatus) flightDataSvc.WithMissionResolver(services.Mission) flightDataSvc.WithFlightLookup(services.Flight) flightDataSvc.WithFMReportLookup(services.FMReport) flightDataSvc.WithAfterFlightInspectionLookup(services.AfterFlightInspection) services.FMReport.WithUsageRefresher(services.HelicopterUsage) msSSO := service.NewMicrosoftSSO(appCfg.MicrosoftSSO). WithExecutor(resilience.NewExecutor("microsoft_sso", appCfg.Resilience.MicrosoftSSO, infra.Logger, resilience.ClassifyMicrosoftSSOError)). WithConfigResolver(func(ctx context.Context) (config.MicrosoftSSOConfig, error) { cfg, err := services.MasterSettings.LoadMicrosoftEntraRuntimeConfig(ctx) if err == nil { return cfg, nil } // Fallback for local/dev when master settings are missing or stale. // This prevents SSO flows from failing hard when DB-stored settings are invalid. if service.IsMicrosoftSSOConfigComplete(appCfg.MicrosoftSSO) { if infra.Logger != nil { infra.Logger.Warn("microsoft sso runtime config fallback to env", "reason", err.Error()) } return appCfg.MicrosoftSSO, nil } return config.MicrosoftSSOConfig{}, err }) authDeps := service.AuthServiceDependencies{ SSO: msSSO, Protector: protector, Config: appCfg.Auth, Audit: services.Audit, EmailBranding: service.NewAuthEmailBrandingResolver(services.MasterSettings, services.FileManager, infra.FileStorage), EmailOTPEnabled: services.MasterSettings.IsLoginEmailOTPEnabled, DisableLegacyNoopEmailQueue: true, } if appCfg.Queue.OutboxEnabled { authDeps.EmailOutbox = repos.Auth authDeps.EmailSerializer = queueapp.NewOutboxSerializer(appCfg.Queue, appCfg.SQS) } services.Auth = service.NewAuthService( repos.Auth, repos.Role, repos.TokenStore, infra.EmailQueue, authDeps, ) services.Contact = service.NewContactService(repos.Contact, services.Auth) services.HelicopterFile. WithFileManagerService(services.FileManager). WithHelicopterFileEvents(infra.HelicopterFileHub) if infra.FileStorage != nil && !appCfg.FileQueue.OutboxEnabled && strings.TrimSpace(appCfg.FileSQS.QueueURL) == "" { return nil, errors.New("FILE_SQS_QUEUE_URL is required when FILE_QUEUE_OUTBOX_ENABLED=false and file manager upload is enabled") } return services, nil } func initializeHandlers(services *Services, infra *Infrastructure) (*Handlers, error) { if services == nil { return nil, errors.New("services are required") } if infra == nil { return nil, errors.New("infrastructure is required") } fleetStatusHandler := handlers.NewFleetStatusHandler(services.FleetStatus, handlers.FleetStatusDeps{ Helicopter: services.Helicopter, Complaint: services.Complaint, EASA: services.EASARelease, History: services.FleetHistory, MCF: services.MCF, Signoff: services.ActionSignoff, Storage: infra.FileStorage, }) fmReportFlightHandler := handlers.NewFlightHandler(services.Flight). WithAuthService(services.Auth). WithAfterFlightInspectionService(services.AfterFlightInspection). WithFMReportService(services.FMReport). WithFlightDataService(services.FlightData). WithTakeoverService(service.NewTakeoverModuleService(mysql.NewTakeoverRepository(infra.DB))). WithHelicopterService(services.Helicopter). WithReserveAcService(services.ReserveAc). WithMissionService(services.Mission) services.FMReport.WithFleetHistoryBuilder(handlers.NewFMReportFleetHistoryBuilder(fleetStatusHandler, fmReportFlightHandler)) services.FMReport.WithReportCodeResolver(fmReportFlightHandler) return &Handlers{ User: handlers.NewUserHandler(services.User). WithFileManagerService(services.FileManager). WithFileStorage(infra.FileStorage), Contact: handlers.NewContactHandler(services.Contact, services.Auth). WithFileManagerService(services.FileManager). WithFileStorage(infra.FileStorage), Health: handlers.NewHealthHandler(), BuildInfo: handlers.NewBuildInfoHandler(), Auth: handlers.NewAuthHandler(services.Auth).WithFileStorage(infra.FileStorage).WithContactService(services.Contact), Role: handlers.NewRoleHandler(services.Role), Helicopter: handlers.NewHelicopterHandler(services.Helicopter). WithFileManagerService(services.FileManager). WithFileStorage(infra.FileStorage). WithReserveAcService(services.ReserveAc). WithFleetStatusService(services.FleetStatus). WithPINVerifier(services.Auth). WithComplaintService(services.Complaint). WithMCFService(services.MCF). WithHistoryService(services.FleetHistory), HelicopterUsage: handlers.NewHelicopterUsageHandler(services.HelicopterUsage), HelicopterFile: handlers.NewHelicopterFileHandler(services.HelicopterFile). WithFileManagerService(services.FileManager). WithHelicopterService(services.Helicopter). WithTakeoverService(services.Takeover). WithHelicopterFileEvents(infra.HelicopterFileHub). WithFileStorage(infra.FileStorage), Flight: handlers.NewFlightHandler(services.Flight). WithAuthService(services.Auth). WithAfterFlightInspectionService(services.AfterFlightInspection). WithFMReportService(services.FMReport). WithFlightDataService(services.FlightData). WithTakeoverService(service.NewTakeoverModuleService(mysql.NewTakeoverRepository(infra.DB))). WithHelicopterService(services.Helicopter). WithReserveAcService(services.ReserveAc). WithMissionService(services.Mission), FlightData: handlers.NewFlightDataHandler(services.FlightData). WithMissionService(services.Mission). WithUserService(services.User). WithHospitalService(services.Hospital). WithICAOService(services.ICAO). WithFacilityService(services.Facility), Mission: handlers.NewMissionHandler(services.Mission).WithFlightDataService(services.FlightData).WithFileStorage(infra.FileStorage).WithFileManagerService(services.FileManager), FMReport: handlers.NewFMReportHandler(services.FMReport). WithFlightHandler(fmReportFlightHandler). WithMissionHandler(handlers.NewMissionHandler(services.Mission).WithFlightDataService(services.FlightData).WithFileStorage(infra.FileStorage).WithFileManagerService(services.FileManager)). WithFlightInspectionService(services.FlightInspection). WithAfterFlightInspectionService(services.AfterFlightInspection). WithHelicopterUsageService(services.HelicopterUsage). WithFleetStatusHandler(fleetStatusHandler). WithTakeoverService(services.Takeover). WithHelicopterService(services.Helicopter). WithFileStorage(infra.FileStorage). WithFileManagerService(services.FileManager), ReserveAc: handlers.NewReserveAcHandler(services.ReserveAc). WithFlightService(services.Flight). WithFlightDataService(services.FlightData). WithMissionService(services.Mission). WithDutyRosterService(services.DutyRoster). WithHelicopterService(services.Helicopter). WithFlightInspectionService(services.FlightInspection). WithBeforeFlightInspectionService(services.BeforeFlightInspection). WithAfterFlightInspectionService(services.AfterFlightInspection). WithFMReportService(services.FMReport). WithFlightPrepCheckService(services.FlightPrepCheck). WithHelicopterFileService(services.HelicopterFile). WithFlightInspectionFileChecklistService(services.FlightInspectionChecklist). WithFileStorage(infra.FileStorage), Takeover: handlers.NewTakeoverHandler(infra.DB). WithAuthService(services.Auth). WithBaseService(services.Base). WithFlightService(services.Flight). WithFileManagerService(services.FileManager). WithTakeoverAcService(service.NewTakeoverModuleService(mysql.NewTakeoverRepository(infra.DB))). WithReserveAcService(services.ReserveAc). WithDutyRosterService(services.DutyRoster). WithHelicopterService(services.Helicopter). WithComplaintService(services.Complaint). WithMCFService(services.MCF). WithFleetStatusService(services.FleetStatus). WithFlightInspectionService(services.FlightInspection). WithBeforeFlightInspectionService(services.BeforeFlightInspection). WithFlightPrepCheckService(services.FlightPrepCheck). WithHelicopterFileService(services.HelicopterFile). WithFlightInspectionFileChecklistService(services.FlightInspectionChecklist). WithFileStorage(infra.FileStorage), OtherPerson: handlers.NewOtherPersonHandler(services.OtherPerson), Facility: handlers.NewFacilityHandler(services.Facility), Base: handlers.NewBaseHandler(services.Base).WithFileManagerService(services.FileManager).WithFileStorage(infra.FileStorage), Medicine: handlers.NewMedicineHandler(services.Medicine), Vocation: handlers.NewVocationHandler(services.Vocation), Opc: handlers.NewOpcHandler(services.Opc), ForcesPresent: handlers.NewForcesPresentHandler(services.ForcesPresent), Hospital: handlers.NewHospitalHandler(services.Hospital), HealthInsuranceCompanies: handlers.NewHealthInsuranceCompaniesHandler(services.HealthInsuranceCompanies), FederalState: handlers.NewFederalStateHandler(services.FederalState), ICAO: handlers.NewICAOHandler(services.ICAO), Land: handlers.NewLandHandler(services.Land), MasterSettings: handlers.NewMasterSettingsHandler(services.MasterSettings, services.FileManager, infra.FileStorage), Branding: handlers.NewBrandingHandler(services.MasterSettings, services.FileManager, infra.FileStorage), FileManagerFolder: handlers.NewFileManagerFolderHandler(services.FileManager, infra.FileStorage), FileManagerFile: handlers.NewFileManagerFileHandler(services.FileManager, infra.FileStorage). WithHelicopterFileService(services.HelicopterFile). WithAircraftService(services.Helicopter). WithBaseService(services.Base). WithDULService(services.DUL). WithAuthService(services.Auth). WithTakeoverService(services.Takeover). WithMissionService(services.Mission). WithUserService(services.User). WithFleetStatusFileService(service.NewFleetStatusFileService(services.FileManager)). WithHelicopterFileEvents(infra.HelicopterFileHub). WithWOPIService(services.WOPI), FileManagerAttachment: handlers.NewFileManagerAttachmentHandler(services.FileManager), FileManagerEvents: handlers.NewFileManagerEventsHandler(infra.FileStatusHub), HelicopterFileEvents: handlers.NewHelicopterFileEventsHandler(infra.HelicopterFileHub), DutyRoster: handlers.NewDutyRosterHandler(services.DutyRoster).WithAudit(services.Audit).WithFlightService(services.Flight), AirRescuerChecklist: handlers.NewAirRescuerChecklistHandler(services.AirRescuerChecklist), InsurancePatientData: handlers.NewInsurancePatientDataHandler(services.InsurancePatientData), PatientData: handlers.NewPatientDataHandler(services.PatientData), DUL: handlers.NewDULHandler(services.DUL).WithBaseService(services.Base).WithContactService(services.Contact).WithFileManagerService(services.FileManager).WithFileStorage(infra.FileStorage), FleetStatus: fleetStatusHandler, HEMSOperationalData: handlers.NewHEMSOperationalDataHandler(services.HEMSOperationalData).WithFileManagerService(services.FileManager), HEMSOperationCategory: handlers.NewHEMSOperationCategoryHandler(services.HEMSOperationCategory), HEMSOperation: handlers.NewHEMSOperationHandler(services.HEMSOperation), Complaint: handlers.NewComplaintHandler(services.Complaint).WithHelicopterService(services.Helicopter).WithMCFService(services.MCF).WithHistoryService(services.FleetHistory).WithAOGRecalculator(services.FleetStatus).WithActionSignoffService(services.ActionSignoff), EASARelease: handlers.NewEASAReleaseHandler(services.EASARelease).WithComplaintActionChecker(services.Complaint).WithActionSignoffChecker(services.ActionSignoff).WithHistoryService(services.FleetHistory).WithContactResolver(services.Contact).WithAOGRecalculator(services.FleetStatus), ActionSignoff: handlers.NewActionSignoffHandler(services.ActionSignoff).WithComplaintService(services.Complaint), MCF: handlers.NewMCFHandler(services.MCF).WithHistoryService(services.FleetHistory), }, nil } func initializeRealtimeComponents( ctx context.Context, repos *Repositories, services *Services, infra *Infrastructure, ) error { if repos == nil || services == nil || infra == nil { return errors.New("repositories, services, and infrastructure are required") } userNameCache := userctx.NewUserNameCache(repos.User, 5*time.Minute) handlers.WireUserNameGetter(userNameCache) startFileManagerAutoPurgeLoop(ctx, services.FileManager, infra.FileTrashPolicy, infra.Logger) startFileManagerLifecycleCleanupLoop(ctx, services.FileManager, infra.FileLifecycle, infra.Logger) startHelicopterFileAttachmentReconcileLoop(ctx, services.HelicopterFile, infra.Logger) startFleetStatusAOGRecomputeLoop(ctx, services.FleetStatus, infra.Logger) fileStatusPoller := filemanagerrealtime.NewStatusPoller(infra.DB, infra.FileStatusHub, infra.Logger, filemanagerrealtime.StatusPollerConfig{ // Keep the SSE ready event responsive; this poller is the bridge between // the async file processor and the live file.updated stream. PollInterval: 250 * time.Millisecond, BatchSize: 100, OnPublished: func(ctx context.Context, event filemanagerrealtime.FileStatusEvent) error { if filemanager.NormalizeFileStatus(event.Status) != filemanager.FileStatusReady { return nil } if services == nil || services.HelicopterFile == nil { return nil } finalized, err := services.HelicopterFile.FinalizePendingAttachmentsBySourceFileUUID(ctx, event.FileUUID, 50) if err != nil { return err } if finalized > 0 && infra.Logger != nil { infra.Logger.Info("helicopter file attachments finalized from file ready event", "file_uuid", event.FileUUID, "finalized", finalized) } return nil }, }) go func() { if err := fileStatusPoller.Run(ctx); err != nil && ctx.Err() == nil { infra.Logger.Error("file status poller stopped unexpectedly", "error", err) } }() return nil } func buildHTTPServer(appCfg config.AppConfig, services *Services, handlersSet *Handlers, infra *Infrastructure) (*fiber.App, string, error) { if services == nil || handlersSet == nil || infra == nil { return nil, "", errors.New("services, handlers, and infra are required") } httpCfg := appCfg.HTTP app := fiber.New(fiber.Config{ BodyLimit: httpCfg.BodyLimit, ReadTimeout: httpCfg.ReadTimeout, WriteTimeout: httpCfg.WriteTimeout, IdleTimeout: httpCfg.IdleTimeout, }) api.UseDefaultMiddlewares(app, httpCfg) envCfg := config.LoadEnvConfig() hasJSONSAML := appCfg.SAML.CertFile != "" || appCfg.SAML.KeyFile != "" || appCfg.SAML.MetadataFile != "" || appCfg.SAML.RootURL != "" || appCfg.SAML.FrontendURL != "" || appCfg.SAML.CookieDomain != "" || appCfg.SAML.SessionTTLHr > 0 if appCfg.SAML.CertFile != "" { envCfg.SAMLCertFile = appCfg.SAML.CertFile } if appCfg.SAML.KeyFile != "" { envCfg.SAMLKeyFile = appCfg.SAML.KeyFile } if appCfg.SAML.MetadataFile != "" { envCfg.SAMLMetadataFile = appCfg.SAML.MetadataFile } if appCfg.SAML.RootURL != "" { envCfg.SAMLRootURL = appCfg.SAML.RootURL } if appCfg.SAML.FrontendURL != "" { envCfg.FrontendURL = appCfg.SAML.FrontendURL } if appCfg.SAML.CookieDomain != "" { envCfg.CookieDomain = appCfg.SAML.CookieDomain } if hasJSONSAML { envCfg.CookieSecure = appCfg.SAML.CookieSecure } if appCfg.SAML.SessionTTLHr > 0 { envCfg.SessionTTLHour = appCfg.SAML.SessionTTLHr } // Microsoft auth mode selector. Default "oauth" uses the OIDC/OAuth2 authorization // code flow (Microsoft refresh tokens are stored in DB and validated on // /api/v1/auth/refresh). Set MICROSOFT_AUTH_MODE=saml to fall back to the legacy // SAML flow (kept intact for rollback). microsoftAuthMode := strings.ToLower(strings.TrimSpace(os.Getenv("MICROSOFT_AUTH_MODE"))) if microsoftAuthMode == "" { microsoftAuthMode = "oauth" } var samlHandler *samlauth.Handler if microsoftAuthMode == "saml" { if err := samlauth.InitSessionStoreDB(infra.DB); err == nil { if err := samlauth.InitSAML(envCfg); err == nil { samlHandler = samlauth.NewHandler(envCfg, services.Auth) } } } slog.Info("microsoft auth mode configured", slog.String("mode", microsoftAuthMode), slog.Bool("saml_active", samlHandler != nil)) api.RegisterRoutes(app, api.RouteDependencies{ Handlers: api.RouteHandlers{ User: handlersSet.User, Contact: handlersSet.Contact, Health: handlersSet.Health, BuildInfo: handlersSet.BuildInfo, Auth: handlersSet.Auth, Role: handlersSet.Role, Helicopter: handlersSet.Helicopter, HelicopterUsage: handlersSet.HelicopterUsage, HelicopterFile: handlersSet.HelicopterFile, Flight: handlersSet.Flight, FlightData: handlersSet.FlightData, Mission: handlersSet.Mission, FMReport: handlersSet.FMReport, ReserveAc: handlersSet.ReserveAc, Takeover: handlersSet.Takeover, OtherPerson: handlersSet.OtherPerson, Facility: handlersSet.Facility, Base: handlersSet.Base, Medicine: handlersSet.Medicine, Vocation: handlersSet.Vocation, Opc: handlersSet.Opc, ForcesPresent: handlersSet.ForcesPresent, Hospital: handlersSet.Hospital, HealthInsuranceCompanies: handlersSet.HealthInsuranceCompanies, FederalState: handlersSet.FederalState, ICAO: handlersSet.ICAO, Land: handlersSet.Land, MasterSettings: handlersSet.MasterSettings, Branding: handlersSet.Branding, FileManagerFolder: handlersSet.FileManagerFolder, FileManagerFile: handlersSet.FileManagerFile, FileManagerAttachment: handlersSet.FileManagerAttachment, FileManagerEvents: handlersSet.FileManagerEvents, HelicopterFileEvents: handlersSet.HelicopterFileEvents, DutyRoster: handlersSet.DutyRoster, AirRescuerChecklist: handlersSet.AirRescuerChecklist, InsurancePatientData: handlersSet.InsurancePatientData, PatientData: handlersSet.PatientData, DUL: handlersSet.DUL, FleetStatus: handlersSet.FleetStatus, HEMSOperationalData: handlersSet.HEMSOperationalData, HEMSOperationCategory: handlersSet.HEMSOperationCategory, HEMSOperation: handlersSet.HEMSOperation, Complaint: handlersSet.Complaint, EASARelease: handlersSet.EASARelease, ActionSignoff: handlersSet.ActionSignoff, MCF: handlersSet.MCF, SAMLAuth: samlHandler, }, Services: api.RouteServices{ Auth: services.Auth, Audit: services.Audit, WOPI: services.WOPI, }, }) addr := ":8080" if appCfg.HTTPPort > 0 { addr = fmt.Sprintf(":%d", appCfg.HTTPPort) } return app, addr, nil }