Files
carvalho-finances/apps/api/cmd/server/main.go
T
Mlcavalho1 f60ca423d0 feat(schema): #48 monthly_budget em categories + category_id em achievements
- Migration 018: ADD COLUMN monthly_budget NUMERIC(12,2) em categories
- Migration 018: ADD COLUMN category_id INTEGER REFERENCES categories(id) ON DELETE CASCADE em achievements
- Remove conquistas hardcoded veleiro_under_500 e health_3months do banco
- Reset game state do profile_id=1
- Atualiza CategoryRepository, CategoryInput e Category model para incluir monthly_budget
2026-05-28 23:26:39 -03:00

186 lines
6.2 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)
}
// CLI subcommand: create-user
if len(os.Args) > 1 && os.Args[1] == "create-user" {
authmw.RunCreateUser(pool)
return
}
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
r := chi.NewRouter()
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
gameRepo := repository.NewGameRepository(pool)
gameSvc := service.NewGameService(gameRepo)
gameHandler := handler.NewGameHandler(gameSvc)
categoryRepo := repository.NewCategoryRepository(pool)
categorySvc := service.NewCategoryService(categoryRepo)
categoryHandler := handler.NewCategoryHandler(categorySvc, gameSvc)
transactionRepo := repository.NewTransactionRepository(pool)
importSvc := service.NewImportService(transactionRepo)
pendingBillRepo := repository.NewPendingBillRepository(pool)
pendingBillSvc := service.NewPendingBillService(pendingBillRepo, transactionRepo)
importHandler := handler.NewImportHandler(importSvc, gameSvc, pendingBillSvc)
pendingBillHandler := handler.NewPendingBillHandler(pendingBillSvc, 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)
cdiSvc := service.NewCDIYieldService(accountRepo, manualTxRepo)
creditBillRepo := repository.NewCreditBillRepository(pool)
creditBillSvc := service.NewCreditBillService(creditBillRepo, accountRepo)
accountSvc := service.NewAccountService(accountRepo, cdiSvc, creditBillSvc)
accountHandler := handler.NewAccountHandler(accountSvc)
creditBillHandler := handler.NewCreditBillHandler(creditBillSvc)
settingsRepo := repository.NewSettingsRepository(pool)
settingsSvc := service.NewSettingsService(settingsRepo)
settingsHandler := handler.NewSettingsHandler(settingsSvc)
dashboardRepo := repository.NewDashboardRepository(pool)
dashboardSvc := service.NewDashboardService(dashboardRepo, recurringSvc, accountRepo, creditBillSvc, pendingBillSvc, settingsSvc)
dashboardHandler := handler.NewDashboardHandler(dashboardSvc, gameSvc)
r.Get("/health", handler.Health)
// Public auth endpoints
r.Post("/api/auth/login", authmw.LoginHandler(pool))
r.Post("/api/logout", authmw.LogoutHandler)
// Protected routes
r.Route("/api", func(r chi.Router) {
r.Use(authmw.RequireAuth)
r.Get("/auth/me", authmw.MeHandler)
r.Post("/auth/change-password", authmw.ChangePasswordHandler(pool))
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("/pending-bills", pendingBillHandler.List)
r.Post("/pending-bills/{id}/confirm", pendingBillHandler.Confirm)
r.Delete("/pending-bills/{id}", pendingBillHandler.Discard)
r.Get("/transactions", txHandler.List)
r.Post("/transactions", txHandler.Create)
r.Put("/transactions/{id}", txHandler.Update)
r.Delete("/transactions/{id}", txHandler.Delete)
r.Delete("/transactions", txHandler.DeleteByMonth)
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.Post("/recurring/{id}/confirm", recurringHandler.ConfirmIncome)
r.Post("/recurring/{id}/late", recurringHandler.MarkLate)
r.Get("/accounts", accountHandler.List)
r.Post("/accounts", accountHandler.Create)
r.Put("/accounts/{id}", accountHandler.Update)
r.Delete("/accounts/{id}", accountHandler.Delete)
r.Get("/accounts/{id}/bills", creditBillHandler.ListByAccount)
r.Get("/accounts/{id}/bills/current", creditBillHandler.GetCurrent)
r.Post("/accounts/{id}/bills/ensure", creditBillHandler.EnsureCurrent)
r.Post("/bills/{id}/pay", creditBillHandler.MarkPaid)
r.Get("/dashboard", dashboardHandler.Get)
r.Get("/settings", settingsHandler.Get)
r.Put("/settings", settingsHandler.Update)
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)
}
}