package httpapp import ( "fmt" "html/template" "net/http" "os" "path/filepath" "time" "gitea.avt.data-center.id/othman.suseno/atlas/internal/audit" "gitea.avt.data-center.id/othman.suseno/atlas/internal/auth" "gitea.avt.data-center.id/othman.suseno/atlas/internal/backup" "gitea.avt.data-center.id/othman.suseno/atlas/internal/db" "gitea.avt.data-center.id/othman.suseno/atlas/internal/job" "gitea.avt.data-center.id/othman.suseno/atlas/internal/maintenance" "gitea.avt.data-center.id/othman.suseno/atlas/internal/metrics" "gitea.avt.data-center.id/othman.suseno/atlas/internal/services" "gitea.avt.data-center.id/othman.suseno/atlas/internal/snapshot" "gitea.avt.data-center.id/othman.suseno/atlas/internal/storage" "gitea.avt.data-center.id/othman.suseno/atlas/internal/tls" "gitea.avt.data-center.id/othman.suseno/atlas/internal/zfs" ) type Config struct { Addr string TemplatesDir string StaticDir string DatabasePath string // Path to SQLite database (empty = in-memory mode) } type App struct { cfg Config tmpl *template.Template mux *http.ServeMux zfs *zfs.Service snapshotPolicy *snapshot.PolicyStore jobManager *job.Manager scheduler *snapshot.Scheduler authService *auth.Service userStore *auth.UserStore auditStore *audit.Store smbStore *storage.SMBStore nfsStore *storage.NFSStore iscsiStore *storage.ISCSIStore database *db.DB // Optional database connection smbService *services.SMBService nfsService *services.NFSService iscsiService *services.ISCSIService metricsCollector *metrics.Collector startTime time.Time backupService *backup.Service maintenanceService *maintenance.Service tlsConfig *tls.Config } func New(cfg Config) (*App, error) { if cfg.TemplatesDir == "" { cfg.TemplatesDir = "web/templates" } if cfg.StaticDir == "" { cfg.StaticDir = "web/static" } tmpl, err := parseTemplates(cfg.TemplatesDir) if err != nil { return nil, err } zfsService := zfs.New() policyStore := snapshot.NewPolicyStore() jobMgr := job.NewManager() scheduler := snapshot.NewScheduler(policyStore, zfsService, jobMgr) // Initialize database (optional) var database *db.DB if cfg.DatabasePath != "" { dbConn, err := db.New(cfg.DatabasePath) if err != nil { return nil, fmt.Errorf("init database: %w", err) } database = dbConn } // Initialize auth jwtSecret := os.Getenv("ATLAS_JWT_SECRET") authService := auth.New(jwtSecret) userStore := auth.NewUserStore(authService) // Initialize audit logging (keep last 10000 logs) auditStore := audit.NewStore(10000) // Initialize storage services smbStore := storage.NewSMBStore() nfsStore := storage.NewNFSStore() iscsiStore := storage.NewISCSIStore() // Initialize service daemon integrations smbService := services.NewSMBService() nfsService := services.NewNFSService() iscsiService := services.NewISCSIService() // Initialize metrics collector metricsCollector := metrics.NewCollector() startTime := time.Now() // Initialize backup service backupDir := os.Getenv("ATLAS_BACKUP_DIR") if backupDir == "" { backupDir = "data/backups" } backupService, err := backup.New(backupDir) if err != nil { return nil, fmt.Errorf("init backup service: %w", err) } // Initialize maintenance service maintenanceService := maintenance.NewService() // Initialize TLS configuration tlsConfig := tls.LoadConfig() if err := tlsConfig.Validate(); err != nil { return nil, fmt.Errorf("TLS configuration: %w", err) } a := &App{ cfg: cfg, tmpl: tmpl, mux: http.NewServeMux(), zfs: zfsService, snapshotPolicy: policyStore, jobManager: jobMgr, scheduler: scheduler, authService: authService, userStore: userStore, auditStore: auditStore, smbStore: smbStore, nfsStore: nfsStore, iscsiStore: iscsiStore, database: database, smbService: smbService, nfsService: nfsService, iscsiService: iscsiService, metricsCollector: metricsCollector, startTime: startTime, backupService: backupService, maintenanceService: maintenanceService, tlsConfig: tlsConfig, } // Start snapshot scheduler (runs every 15 minutes) scheduler.Start(15 * time.Minute) a.routes() return a, nil } func (a *App) Router() http.Handler { // Middleware chain order (outer to inner): // 1. HTTPS enforcement (redirect HTTP to HTTPS) // 2. CORS (handles preflight) // 3. Compression (gzip) // 4. Security headers // 5. Request size limit (10MB) // 6. Content-Type validation // 7. Rate limiting // 8. Caching (for GET requests) // 9. Error recovery // 10. Request ID // 11. Logging // 12. Audit // 13. Authentication // 14. Maintenance mode (blocks operations during maintenance) // 15. Routes return a.httpsEnforcementMiddleware( a.corsMiddleware( a.compressionMiddleware( a.securityHeadersMiddleware( a.requestSizeMiddleware(10 * 1024 * 1024)( a.validateContentTypeMiddleware( a.rateLimitMiddleware( a.cacheMiddleware( a.errorMiddleware( requestID( logging( a.auditMiddleware( a.maintenanceMiddleware( a.authMiddleware(a.mux), ), ), ), ), ), ), ), ), ), ), ), ), ) } // StopScheduler stops the snapshot scheduler (for graceful shutdown) func (a *App) StopScheduler() { if a.scheduler != nil { a.scheduler.Stop() } // Close database connection if present if a.database != nil { a.database.Close() } } // routes() is now in routes.go func parseTemplates(dir string) (*template.Template, error) { pattern := filepath.Join(dir, "*.html") files, err := filepath.Glob(pattern) if err != nil { return nil, err } // Allow empty templates for testing if len(files) == 0 { // Return empty template instead of error for testing return template.New("root"), nil } funcs := template.FuncMap{ "nowRFC3339": func() string { return time.Now().Format(time.RFC3339) }, } t := template.New("root").Funcs(funcs) return t.ParseFiles(files...) }