Files
fm_be/cmd/api/startup.go
2026-07-16 22:16:45 +07:00

835 lines
41 KiB
Go

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
}