Files
initpage/cmd/server/main.go
2026-10-22 00:11:59 -03:00

139 lines
3.6 KiB
Go

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/<site>/.
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))
}