Files
atlas/internal/httpapp/app.go

271 lines
7.1 KiB
Go

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) {
// Resolve paths relative to executable or current working directory
if cfg.TemplatesDir == "" {
// Try multiple locations for templates
possiblePaths := []string{
"web/templates",
"./web/templates",
"/opt/atlas/web/templates",
}
for _, path := range possiblePaths {
if _, err := os.Stat(path); err == nil {
cfg.TemplatesDir = path
break
}
}
if cfg.TemplatesDir == "" {
cfg.TemplatesDir = "web/templates" // Default fallback
}
}
if cfg.StaticDir == "" {
// Try multiple locations for static files
possiblePaths := []string{
"web/static",
"./web/static",
"/opt/atlas/web/static",
}
for _, path := range possiblePaths {
if _, err := os.Stat(path); err == nil {
cfg.StaticDir = path
break
}
}
if cfg.StaticDir == "" {
cfg.StaticDir = "web/static" // Default fallback
}
}
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) },
"getContentTemplate": func(data map[string]any) string {
if ct, ok := data["ContentTemplate"].(string); ok && ct != "" {
return ct
}
return "content"
},
}
t := template.New("root").Funcs(funcs)
return t.ParseFiles(files...)
}