inboxer/src/cmd/main.go
cclohmar 01a58156a1 Consolidate all secrets into config.yaml — remove .env entirely
All configuration (including secrets) now lives in a single file:
bin/config.yaml. The separate .env file has been eliminated.

Changes:
- config.go: Added SMTPSettings struct + AI.APIKey to Config; removed
  godotenv import, Environment struct, and all os.Getenv() calls
- config.yaml: Added smtp section (host/port/username/password) and
  ai.api_key field with placeholder values
- main.go: Reads SMTP and API key from cfg instead of env
- smtp.go: Changed Port field from string to int
- otp_test.go: Updated Port values to int
- .env.example: Deleted (all config is in config.yaml)
- .gitignore: Removed .env.example; kept .env for safety
- go.mod/go.sum: Removed github.com/joho/godotenv dependency
- install.sh: No longer creates .env or uses EnvironmentFile;
  warns about placeholder values in config.yaml instead
2026-04-23 20:06:16 +00:00

178 lines
No EOL
4.8 KiB
Go

package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"inboxer/src/internal/ai"
"inboxer/src/internal/auth"
"inboxer/src/internal/db"
"inboxer/src/internal/web"
"inboxer/src/internal/worker"
"inboxer/src/pkg/config"
"github.com/gorilla/mux"
)
func main() {
// Load configuration
configPath, err := config.GetDefaultConfigPath()
if err != nil {
configPath = "bin/config.yaml"
}
cfg, err := config.LoadConfig(configPath)
if err != nil {
log.Fatalf("Failed to load configuration: %v", err)
}
// Initialize database
database, err := db.NewDatabase(cfg.Database.Path, cfg.Server.SessionSecret)
if err != nil {
log.Fatalf("Failed to initialize database: %v", err)
}
defer database.Close()
// Initialize SMTP sender (credentials from config.yaml)
smtpConfig := auth.SMTPConfig{
Host: cfg.SMTP.Host,
Port: cfg.SMTP.Port,
Username: cfg.SMTP.Username,
Password: cfg.SMTP.Password,
From: cfg.SMTP.Username, // From address matches SMTP username
}
// Validate SMTP config (but don't fail if not set — user might configure later)
if err := smtpConfig.ValidateConfig(); err != nil {
log.Printf("Warning: SMTP configuration incomplete: %v", err)
log.Println("OTP emails will not be sent until SMTP is configured in config.yaml")
}
smtpSender := auth.NewSMTPSender(smtpConfig)
// Initialize session manager
sessionManager := auth.NewSessionManager(cfg.Server.SessionSecret)
// In development, allow non-HTTPS
if os.Getenv("APP_ENV") == "development" {
sessionManager.UpdateSessionOptions(false, 86400*7)
}
// Initialize OTP store (database-backed)
otpStore := db.NewDatabaseOTPStore(database)
// Initialize auth service
authService := auth.NewAuthService(smtpSender, sessionManager, otpStore)
// Initialize AI classifier
deepSeekAPI := ai.NewDeepSeekAPI(
cfg.AI.APIKey,
cfg.AI.Model,
cfg.AI.MaxTokens,
cfg.AI.Temperature,
)
var classifier worker.AIClassifier
classifier, err = ai.NewClassifier(deepSeekAPI, cfg.AI.PromptFile)
if err != nil {
log.Printf("Warning: AI classifier initialization failed: %v", err)
log.Println("Falling back to placeholder classifier (all emails -> Other)")
classifier = worker.NewPlaceholderClassifier(cfg.Folders.Other)
}
// Create background worker (not started yet — handlers need a reference first)
bgWorker := worker.NewWorker(database, cfg, classifier)
// Initialize web handlers (needs worker for ProcessNow)
handler, err := web.NewHandler(authService, database, cfg, bgWorker)
if err != nil {
log.Fatalf("Failed to initialize web handlers: %v", err)
}
// Setup router
router := mux.NewRouter()
handler.RegisterRoutes(router)
// Add middleware
router.Use(loggingMiddleware)
router.Use(authMiddleware(authService))
// Create HTTP server
server := &http.Server{
Addr: fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port),
Handler: router,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second,
}
// Start background worker (now that everything is wired up)
bgWorker.Start()
// Start server in goroutine
go func() {
log.Printf("Starting server on %s", server.Addr)
log.Printf("Access the application at http://localhost:%d", cfg.Server.Port)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Server error: %v", err)
}
}()
// Wait for interrupt signal
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
<-stop
log.Println("Shutting down...")
// Stop background worker first
bgWorker.Stop()
// Give server time to shutdown gracefully
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Printf("Server shutdown error: %v", err)
}
log.Println("Server stopped")
}
// loggingMiddleware logs HTTP requests
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
})
}
// authMiddleware checks authentication for protected routes
func authMiddleware(authService *auth.AuthService) mux.MiddlewareFunc {
protectedPaths := map[string]bool{
"/dashboard": true,
"/settings": true,
"/logout": true,
"/toggle-test-mode": true,
"/test-connection": true,
"/process-now": true,
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Check if path is protected
if protectedPaths[r.URL.Path] {
// Check if user is logged in
if !authService.GetSessionManager().IsLoggedIn(r) {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
}
next.ServeHTTP(w, r)
})
}
}