fix: adapt for subpath reverse proxies, embed web assets and fix relative URLs
This commit is contained in:
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
157
main.go
Normal file
157
main.go
Normal file
@@ -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))
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<base href="/initpage/">
|
||||
<base href="./">
|
||||
<title>InitPage — Seu navegador, personalizado</title>
|
||||
<meta name="description" content="Pagina inicial personalizavel com atalhos, categorias, busca e plano de fundo. Tudo salvo localmente no seu navegador.">
|
||||
<meta name="theme-color" content="#0f172a">
|
||||
@@ -270,9 +270,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 text-[11px] text-slate-500">
|
||||
<a href="https://weltonmoura.com.br/initpage/politica-privacidade/" class="hover:text-white transition">Política de Privacidade</a>
|
||||
<a href="./politica-privacidade/" class="hover:text-white transition">Política de Privacidade</a>
|
||||
<span class="text-slate-700">•</span>
|
||||
<a href="https://weltonmoura.com.br/initpage/termos-servico/" class="hover:text-white transition">Termos de Serviço</a>
|
||||
<a href="./termos-servico/" class="hover:text-white transition">Termos de Serviço</a>
|
||||
<span class="text-slate-700">•</span>
|
||||
<span>by <a href="https://weltonmoura.com.br/" class="hover:text-amber-400 text-white transition">Welton Moura</a></span>
|
||||
</div>
|
||||
|
||||
@@ -158,9 +158,9 @@
|
||||
</section>
|
||||
|
||||
<footer class="mt-8 text-center text-xs text-slate-500 space-x-4">
|
||||
<a class="accent" href="https://weltonmoura.com.br/initpage/termos-servico/">Termos de Serviço</a>
|
||||
<a class="accent" href="../termos-servico/">Termos de Serviço</a>
|
||||
<span>•</span>
|
||||
<a class="accent" href="https://weltonmoura.com.br/initpage/">Voltar ao início</a>
|
||||
<a class="accent" href="../">Voltar ao início</a>
|
||||
</footer>
|
||||
</main>
|
||||
</body>
|
||||
|
||||
@@ -153,9 +153,9 @@
|
||||
</section>
|
||||
|
||||
<footer class="mt-8 text-center text-xs text-slate-500 space-x-4">
|
||||
<a class="accent" href="https://weltonmoura.com.br/initpage/politica-privacidade/">Política de Privacidade</a>
|
||||
<a class="accent" href="../politica-privacidade/">Política de Privacidade</a>
|
||||
<span>•</span>
|
||||
<a class="accent" href="https://weltonmoura.com.br/initpage/">Voltar ao início</a>
|
||||
<a class="accent" href="../">Voltar ao início</a>
|
||||
</footer>
|
||||
</main>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user