package main import ( "embed" "io" "io/fs" "log" "net/http" "os" "path/filepath" "strings" "time" "github.com/alexedwards/scs/v2" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/joho/godotenv" "golang.org/x/time/rate" "github.com/weltonmoura/initpage/internal/config" "github.com/weltonmoura/initpage/internal/db" "github.com/weltonmoura/initpage/internal/handlers" mw "github.com/weltonmoura/initpage/internal/middleware" "github.com/weltonmoura/initpage/internal/sync" ) //go:embed web/* var embeddedWeb embed.FS func main() { _ = godotenv.Load() cfg := config.Load() dbConn, err := db.Init(cfg.DBPath) if err != nil { log.Fatalf("[InitPage] Falha ao inicializar banco de dados: %v", err) } defer dbConn.Close() sessionManager := scs.New() sessionManager.Lifetime = 24 * time.Hour sessionManager.Cookie.Name = "initpage_session" sessionManager.Cookie.HttpOnly = true sessionManager.Cookie.SameSite = http.SameSiteLaxMode sessionManager.Cookie.Secure = strings.HasPrefix(cfg.AppOrigin, "https://") authH := &handlers.Auth{DB: dbConn, SM: sessionManager, Cfg: cfg} syncH := &sync.Sync{DB: dbConn} rl := mw.NewIPRateLimiter(rate.Every(time.Minute/10), 10) rl.Cleanup() r := chi.NewRouter() r.Use(middleware.Logger) r.Use(middleware.Recoverer) r.Use(middleware.RealIP) r.Use(sessionManager.LoadAndSave) // Base path normalizado (ex: "" ou "/initpage") base := strings.TrimRight(cfg.BasePath, "/") if base != "" && !strings.HasPrefix(base, "/") { base = "/" + base } // 1. Rotas de API r.Route(base+"/api", func(api chi.Router) { api.Group(func(g chi.Router) { g.Use(mw.RateLimit(rl)) g.Post("/auth/register", authH.Register) g.Post("/auth/login", authH.Login) }) api.Post("/auth/logout", authH.Logout) api.Get("/auth/me", authH.Me) api.Group(func(g chi.Router) { g.Use(mw.AuthRequired(sessionManager)) g.Use(mw.LoadUserID(sessionManager)) g.Get("/sync", syncH.Get) g.Put("/sync", syncH.Put) }) }) // 2. Sistema de arquivos para o Frontend: // Prioridade 1: Se existir pasta ./web no disco, usa ela (permite edição sem recompilar). // Prioridade 2: Se não existir, usa os arquivos embutidos no binário (standalone no Mini PaaS). var staticFS fs.FS if fi, err := os.Stat("web"); err == nil && fi.IsDir() { log.Println("[InitPage] Servindo frontend a partir da pasta local 'web/' no disco.") staticFS = os.DirFS("web") } else { log.Println("[InitPage] Servindo frontend a partir dos arquivos embutidos no binário (embed).") sub, err := fs.Sub(embeddedWeb, "web") if err != nil { log.Fatalf("[InitPage] Erro ao carregar arquivos estáticos embutidos: %v", err) } staticFS = sub } fileServer := http.FileServer(http.FS(staticFS)) serveFileFromFS := func(w http.ResponseWriter, r *http.Request, name string) bool { f, err := staticFS.Open(name) if err != nil { return false } defer f.Close() st, err := f.Stat() if err != nil || st.IsDir() { return false } if seeker, ok := f.(io.ReadSeeker); ok { http.ServeContent(w, r, name, st.ModTime(), seeker) return true } return false } // Handlers de Frontend handleIndex := func(w http.ResponseWriter, r *http.Request) { if !serveFileFromFS(w, r, "index.html") { http.NotFound(w, r) } } // Rota raiz (ou base) if base != "" { r.Get(base, handleIndex) } r.Get(base+"/", handleIndex) // Rotas estáticas r.Get(base+"/*", func(w http.ResponseWriter, r *http.Request) { p := strings.TrimPrefix(r.URL.Path, base) rel := strings.TrimPrefix(p, "/") if rel == "" || rel == "." { handleIndex(w, r) return } // Tenta servir o arquivo solicitado diretamente if serveFileFromFS(w, r, rel) { return } // Se for diretório (ex: politica-privacidade/), tenta o index.html dentro dele indexPath := filepath.ToSlash(filepath.Join(rel, "index.html")) if serveFileFromFS(w, r, indexPath) { return } // Tenta fallback com http.FileServer http.StripPrefix(base, fileServer).ServeHTTP(w, r) }) log.Printf("[InitPage] Servidor rodando em :%s (base: '%s', db: '%s')", cfg.Port, base, cfg.DBPath) log.Fatal(http.ListenAndServe(":"+cfg.Port, r)) }