package main import ( "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" ) func main() { _ = godotenv.Load() cfg := config.Load() dbConn, err := db.Init(cfg.DBPath) if err != nil { log.Fatal(err) } defer dbConn.Close() sessionManager := scs.New() sessionManager.Lifetime = 24 * time.Hour 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) // ~10 req/min rl.Cleanup() r := chi.NewRouter() r.Use(middleware.Logger) r.Use(middleware.Recoverer) r.Use(middleware.RealIP) r.Use(sessionManager.LoadAndSave) base := strings.TrimRight(cfg.BasePath, "/") 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) }) }) // servir frontend workDir, _ := os.Getwd() webDir := filepath.Join(workDir, "web") fileServer := http.FileServer(http.Dir(webDir)) r.Get(base, func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, filepath.Join(webDir, "index.html")) }) r.Get(base+"/", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, filepath.Join(webDir, "index.html")) }) r.Get(base+"/*", func(w http.ResponseWriter, r *http.Request) { // O BASE_PATH é apenas o prefixo público: /initpage/apps/ aponta para // /app/web/apps/, sem repetir "initpage" dentro da pasta web. p := strings.TrimPrefix(r.URL.Path, base) rel := filepath.Clean(filepath.FromSlash(strings.TrimPrefix(p, "/"))) serveIndex := func(indexPath string) bool { st, err := os.Stat(indexPath) if err != nil || st.IsDir() { return false } http.ServeFile(w, r, indexPath) return true } if rel == "." { serveIndex(filepath.Join(webDir, "index.html")) return } if !filepath.IsLocal(rel) { http.NotFound(w, r) return } full := filepath.Join(webDir, rel) st, err := os.Stat(full) if err == nil && st.IsDir() { if !strings.HasSuffix(r.URL.Path, "/") { http.Redirect(w, r, r.URL.Path+"/", http.StatusMovedPermanently) return } if serveIndex(filepath.Join(full, "index.html")) { return } } else if err == nil { http.StripPrefix(base, fileServer).ServeHTTP(w, r) return } // Em rotas sem extensão, usa o index.html da pasta mais próxima. // Isso permite recarregar rotas internas de SPAs estáticas em web//. if filepath.Ext(rel) == "" { dir := filepath.Dir(rel) for { if serveIndex(filepath.Join(webDir, dir, "index.html")) { return } parent := filepath.Dir(dir) if parent == dir || dir == "." { break } dir = parent } } http.NotFound(w, r) }) log.Printf("Servidor rodando em :%s base=%s", cfg.Port, base) log.Fatal(http.ListenAndServe(":"+cfg.Port, r)) }