- Go: middleware RequireAuth protege todas as rotas /api/* - Go: POST /api/login valida APP_USERNAME/APP_PASSWORD e emite cookie HttpOnly 30 dias - Go: GET /api/auth/me e POST /api/logout - Vue: LoginView com design Arcade Neon - Vue: router guard redireciona para /login se não autenticado - Vue: api.ts redireciona para /login em qualquer 401 - docker-compose: APP_USERNAME e APP_PASSWORD como env vars Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
150 lines
4.5 KiB
Go
150 lines
4.5 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io/fs"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/go-chi/chi/v5/middleware"
|
|
"github.com/joho/godotenv"
|
|
|
|
"financeiro-carvalho/internal/db"
|
|
"financeiro-carvalho/internal/handler"
|
|
authmw "financeiro-carvalho/internal/middleware"
|
|
"financeiro-carvalho/internal/migration"
|
|
"financeiro-carvalho/internal/repository"
|
|
"financeiro-carvalho/internal/service"
|
|
"financeiro-carvalho/static"
|
|
)
|
|
|
|
func main() {
|
|
_ = godotenv.Load()
|
|
|
|
dsn := os.Getenv("DATABASE_URL")
|
|
if dsn == "" {
|
|
log.Fatal("DATABASE_URL is required")
|
|
}
|
|
|
|
ctx := context.Background()
|
|
pool, err := db.Connect(ctx, dsn)
|
|
if err != nil {
|
|
log.Fatalf("db connect: %v", err)
|
|
}
|
|
defer pool.Close()
|
|
|
|
if err := migration.Run(ctx, pool); err != nil {
|
|
log.Fatalf("migration: %v", err)
|
|
}
|
|
|
|
port := os.Getenv("PORT")
|
|
if port == "" {
|
|
port = "8080"
|
|
}
|
|
|
|
r := chi.NewRouter()
|
|
r.Use(middleware.Logger)
|
|
r.Use(middleware.Recoverer)
|
|
|
|
categoryRepo := repository.NewCategoryRepository(pool)
|
|
categorySvc := service.NewCategoryService(categoryRepo)
|
|
categoryHandler := handler.NewCategoryHandler(categorySvc)
|
|
|
|
gameRepo := repository.NewGameRepository(pool)
|
|
gameSvc := service.NewGameService(gameRepo)
|
|
gameHandler := handler.NewGameHandler(gameSvc)
|
|
|
|
transactionRepo := repository.NewTransactionRepository(pool)
|
|
importSvc := service.NewImportService(transactionRepo)
|
|
importHandler := handler.NewImportHandler(importSvc, gameSvc)
|
|
|
|
manualTxRepo := repository.NewManualTransactionRepository(pool)
|
|
txSvc := service.NewTransactionService(manualTxRepo)
|
|
txHandler := handler.NewTransactionHandler(txSvc, gameSvc)
|
|
|
|
recurringRepo := repository.NewRecurringRepository(pool)
|
|
recurringSvc := service.NewRecurringService(recurringRepo, manualTxRepo)
|
|
recurringHandler := handler.NewRecurringHandler(recurringSvc)
|
|
|
|
accountRepo := repository.NewAccountRepository(pool)
|
|
accountSvc := service.NewAccountService(accountRepo)
|
|
accountHandler := handler.NewAccountHandler(accountSvc)
|
|
|
|
dashboardRepo := repository.NewDashboardRepository(pool)
|
|
dashboardSvc := service.NewDashboardService(dashboardRepo, recurringSvc, accountRepo)
|
|
dashboardHandler := handler.NewDashboardHandler(dashboardSvc)
|
|
|
|
r.Get("/health", handler.Health)
|
|
|
|
r.Post("/api/login", authmw.LoginHandler)
|
|
r.Post("/api/logout", authmw.LogoutHandler)
|
|
r.Get("/api/auth/me", authmw.MeHandler)
|
|
|
|
r.Route("/api", func(r chi.Router) {
|
|
r.Use(authmw.RequireAuth)
|
|
r.Get("/categories", categoryHandler.List)
|
|
r.Post("/categories", categoryHandler.Create)
|
|
r.Put("/categories/{id}", categoryHandler.Update)
|
|
r.Delete("/categories/{id}", categoryHandler.Delete)
|
|
|
|
r.Post("/imports/preview", importHandler.Preview)
|
|
r.Post("/imports/confirm", importHandler.Confirm)
|
|
|
|
r.Get("/transactions", txHandler.List)
|
|
r.Post("/transactions", txHandler.Create)
|
|
r.Put("/transactions/{id}", txHandler.Update)
|
|
r.Delete("/transactions/{id}", txHandler.Delete)
|
|
|
|
r.Get("/recurring", recurringHandler.List)
|
|
r.Post("/recurring", recurringHandler.Create)
|
|
r.Put("/recurring/{id}", recurringHandler.Update)
|
|
r.Delete("/recurring/{id}", recurringHandler.Delete)
|
|
r.Get("/recurring/status", recurringHandler.MonthlyStatus)
|
|
r.Post("/recurring/{id}/ignore", recurringHandler.Ignore)
|
|
r.Delete("/recurring/{id}/ignore", recurringHandler.Unignore)
|
|
|
|
r.Get("/accounts", accountHandler.List)
|
|
r.Post("/accounts", accountHandler.Create)
|
|
r.Put("/accounts/{id}", accountHandler.Update)
|
|
r.Delete("/accounts/{id}", accountHandler.Delete)
|
|
|
|
r.Get("/dashboard", dashboardHandler.Get)
|
|
|
|
r.Get("/game/summary", gameHandler.Summary)
|
|
r.Post("/game/xp", gameHandler.AwardXP)
|
|
r.Post("/game/quests/{id}/claim", gameHandler.ClaimQuest)
|
|
r.Get("/game/achievements", gameHandler.Achievements)
|
|
r.Get("/game/cosmetics", gameHandler.Cosmetics)
|
|
r.Post("/game/cosmetics/{id}/equip", gameHandler.EquipCosmetic)
|
|
})
|
|
|
|
// Serve Vue SPA — non-API routes fall through to index.html
|
|
distFS, err := fs.Sub(static.FS, "dist")
|
|
if err != nil {
|
|
log.Fatalf("static embed sub: %v", err)
|
|
}
|
|
fileServer := http.FileServer(http.FS(distFS))
|
|
r.Get("/*", func(w http.ResponseWriter, req *http.Request) {
|
|
path := req.URL.Path[1:]
|
|
if _, statErr := fs.Stat(distFS, path); statErr != nil {
|
|
index, readErr := fs.ReadFile(distFS, "index.html")
|
|
if readErr != nil {
|
|
http.NotFound(w, req)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
w.Write(index)
|
|
return
|
|
}
|
|
fileServer.ServeHTTP(w, req)
|
|
})
|
|
|
|
fmt.Printf("server listening on :%s\n", port)
|
|
if err := http.ListenAndServe(":"+port, r); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|