From dce34f26d3c554a09007eb6770fa95bb08155e65 Mon Sep 17 00:00:00 2001 From: Welton Moura Date: Fri, 23 Oct 2026 14:21:18 -0300 Subject: [PATCH] fix: adapt for subpath reverse proxies, embed web assets and fix relative URLs --- internal/config/config.go | 50 ++++++--- main.go | 157 ++++++++++++++++++++++++++++ web/index.html | 6 +- web/politica-privacidade/index.html | 4 +- web/termos-servico/index.html | 4 +- 5 files changed, 202 insertions(+), 19 deletions(-) create mode 100644 main.go diff --git a/internal/config/config.go b/internal/config/config.go index f425075..e8e697c 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -2,27 +2,53 @@ package config import ( "os" + "path/filepath" ) type Config struct { - Port string - BasePath string - AppOrigin string - SessionSecret string - TurnstileSecret string + Port string + BasePath string + AppOrigin string + SessionSecret string + TurnstileSecret string TurnstileSiteKey string - DBPath string + DBPath string + AppDir string } func Load() *Config { + appDir := os.Getenv("APP_DIR") + if appDir == "" { + appDir = "." + } + + // BasePath: no Mini PaaS, as requisições chegam com prefixo já removido, portanto "" é o ideal. + // Se o usuário passar BASE_PATH explicitamente, respeita a variável. + basePath := os.Getenv("BASE_PATH") + + dbPath := os.Getenv("DB_PATH") + if dbPath == "" { + dbPath = filepath.Join(appDir, "data", "initpage.db") + } + + appOrigin := os.Getenv("APP_ORIGIN") + if appOrigin == "" { + domain := os.Getenv("PAAS_DOMAIN") + if domain == "" { + domain = "paas.weltonmoura.com.br" + } + appOrigin = "https://" + domain + } + return &Config{ - Port: getEnv("PORT", "8080"), - BasePath: getEnv("BASE_PATH", "/initpage"), - AppOrigin: getEnv("APP_ORIGIN", "https://apps.weltonmoura.com.br"), - SessionSecret: getEnv("SESSION_SECRET", "dev-secret-change-me"), - TurnstileSecret: getEnv("TURNSTILE_SECRET", ""), + Port: getEnv("PORT", "8080"), + BasePath: basePath, + AppOrigin: appOrigin, + SessionSecret: getEnv("SESSION_SECRET", "dev-secret-change-me"), + TurnstileSecret: getEnv("TURNSTILE_SECRET", ""), TurnstileSiteKey: getEnv("TURNSTILE_SITEKEY", ""), - DBPath: getEnv("DB_PATH", "./data/initpage.db"), + DBPath: dbPath, + AppDir: appDir, } } diff --git a/main.go b/main.go new file mode 100644 index 0000000..acf00e5 --- /dev/null +++ b/main.go @@ -0,0 +1,157 @@ +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)) +} diff --git a/web/index.html b/web/index.html index b2c1ada..e61c3ae 100644 --- a/web/index.html +++ b/web/index.html @@ -3,7 +3,7 @@ - + InitPage — Seu navegador, personalizado @@ -270,9 +270,9 @@
- Política de Privacidade + Política de Privacidade • - Termos de Serviço + Termos de Serviço • by Welton Moura
diff --git a/web/politica-privacidade/index.html b/web/politica-privacidade/index.html index ea1b46c..f4c114d 100644 --- a/web/politica-privacidade/index.html +++ b/web/politica-privacidade/index.html @@ -158,9 +158,9 @@ diff --git a/web/termos-servico/index.html b/web/termos-servico/index.html index 9497142..463a7ef 100644 --- a/web/termos-servico/index.html +++ b/web/termos-servico/index.html @@ -153,9 +153,9 @@