All checks were successful
CI / ci (push) Successful in 12s
- model/finance.go: FinanceCategory, FinanceTransaction, Summary, Analytics - repository/finance.go: CRUD + summary/analytics queries - service/finance.go: business logic with auto-seed default categories - handler/finance.go: REST endpoints with owner-only check (user_id=1) - db.go: finance_categories + finance_transactions migrations - main.go: register /finance/* routes Endpoints: GET/POST/PUT/DELETE /finance/categories, /finance/transactions GET /finance/summary, /finance/analytics
215 lines
7.5 KiB
Go
215 lines
7.5 KiB
Go
package main
|
|
|
|
import (
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/go-chi/chi/v5/middleware"
|
|
"github.com/go-chi/cors"
|
|
|
|
"github.com/daniil/homelab-api/internal/bot"
|
|
"github.com/daniil/homelab-api/internal/config"
|
|
"github.com/daniil/homelab-api/internal/handler"
|
|
customMiddleware "github.com/daniil/homelab-api/internal/middleware"
|
|
"github.com/daniil/homelab-api/internal/repository"
|
|
"github.com/daniil/homelab-api/internal/scheduler"
|
|
"github.com/daniil/homelab-api/internal/service"
|
|
)
|
|
|
|
func main() {
|
|
cfg := config.Load()
|
|
|
|
// Initialize database
|
|
db, err := repository.NewDB(cfg.DatabaseURL)
|
|
if err != nil {
|
|
log.Fatalf("Failed to connect to database: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
// Run migrations
|
|
if err := repository.RunMigrations(db); err != nil {
|
|
log.Fatalf("Failed to run migrations: %v", err)
|
|
}
|
|
|
|
// Run finance migrations
|
|
if err := repository.RunFinanceMigrations(db); err != nil {
|
|
log.Fatalf("Failed to run finance migrations: %v", err)
|
|
}
|
|
|
|
// Initialize repositories
|
|
userRepo := repository.NewUserRepository(db)
|
|
habitRepo := repository.NewHabitRepository(db)
|
|
taskRepo := repository.NewTaskRepository(db)
|
|
emailTokenRepo := repository.NewEmailTokenRepository(db)
|
|
habitFreezeRepo := repository.NewHabitFreezeRepository(db)
|
|
savingsRepo := repository.NewSavingsRepository(db)
|
|
financeRepo := repository.NewFinanceRepository(db)
|
|
|
|
// Initialize services
|
|
emailService := service.NewEmailService(cfg.ResendAPIKey, cfg.FromEmail, cfg.FromName, cfg.AppURL)
|
|
authService := service.NewAuthService(userRepo, emailTokenRepo, emailService, cfg.JWTSecret)
|
|
habitService := service.NewHabitService(habitRepo, habitFreezeRepo)
|
|
taskService := service.NewTaskService(taskRepo)
|
|
financeService := service.NewFinanceService(financeRepo)
|
|
|
|
// Initialize Telegram bot
|
|
telegramBot, err := bot.New(cfg.TelegramBotToken, userRepo, taskRepo, habitRepo)
|
|
if err != nil {
|
|
log.Printf("Failed to initialize Telegram bot: %v", err)
|
|
}
|
|
|
|
// Start bot in goroutine
|
|
if telegramBot != nil {
|
|
go telegramBot.Start()
|
|
}
|
|
|
|
// Initialize scheduler
|
|
sched := scheduler.New(telegramBot, userRepo, taskRepo, habitRepo, habitFreezeRepo)
|
|
sched.Start()
|
|
defer sched.Stop()
|
|
|
|
// Initialize handlers
|
|
authHandler := handler.NewAuthHandler(authService)
|
|
habitHandler := handler.NewHabitHandler(habitService)
|
|
taskHandler := handler.NewTaskHandler(taskService)
|
|
healthHandler := handler.NewHealthHandler()
|
|
profileHandler := handler.NewProfileHandler(userRepo)
|
|
habitFreezeHandler := handler.NewHabitFreezeHandler(habitFreezeRepo, habitRepo)
|
|
savingsHandler := handler.NewSavingsHandler(savingsRepo)
|
|
interestHandler := handler.NewInterestHandler(db)
|
|
financeHandler := handler.NewFinanceHandler(financeService)
|
|
|
|
// Initialize middleware
|
|
authMiddleware := customMiddleware.NewAuthMiddleware(cfg.JWTSecret)
|
|
|
|
// Setup router
|
|
r := chi.NewRouter()
|
|
|
|
// Global middleware
|
|
r.Use(middleware.Logger)
|
|
r.Use(middleware.Recoverer)
|
|
r.Use(middleware.RequestID)
|
|
r.Use(cors.Handler(cors.Options{
|
|
AllowedOrigins: []string{"https://*", "http://*"},
|
|
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
|
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-Request-ID"},
|
|
ExposedHeaders: []string{"Link"},
|
|
AllowCredentials: true,
|
|
MaxAge: 300,
|
|
}))
|
|
|
|
// Public routes
|
|
r.Get("/health", healthHandler.Health)
|
|
|
|
// Auth routes (public)
|
|
r.Post("/auth/register", authHandler.Register)
|
|
r.Post("/auth/login", authHandler.Login)
|
|
r.Post("/auth/refresh", authHandler.Refresh)
|
|
r.Post("/auth/verify-email", authHandler.VerifyEmail)
|
|
r.Post("/auth/resend-verification", authHandler.ResendVerification)
|
|
r.Post("/auth/forgot-password", authHandler.ForgotPassword)
|
|
r.Post("/auth/reset-password", authHandler.ResetPassword)
|
|
|
|
// Internal routes (API key protected)
|
|
interestHandler.RegisterRoutes(r)
|
|
|
|
// Protected routes
|
|
r.Group(func(r chi.Router) {
|
|
r.Use(authMiddleware.Authenticate)
|
|
|
|
// User routes
|
|
r.Get("/auth/me", authHandler.Me)
|
|
r.Put("/auth/me", authHandler.UpdateProfile)
|
|
r.Put("/auth/password", authHandler.ChangePassword)
|
|
|
|
// Profile routes
|
|
r.Get("/profile", profileHandler.Get)
|
|
r.Put("/profile", profileHandler.Update)
|
|
|
|
// Habits routes
|
|
r.Get("/habits", habitHandler.List)
|
|
r.Post("/habits", habitHandler.Create)
|
|
r.Get("/habits/{id}", habitHandler.Get)
|
|
r.Put("/habits/{id}", habitHandler.Update)
|
|
r.Delete("/habits/{id}", habitHandler.Delete)
|
|
|
|
// Habit logs
|
|
r.Post("/habits/{id}/log", habitHandler.Log)
|
|
r.Get("/habits/{id}/logs", habitHandler.GetLogs)
|
|
r.Delete("/habits/{id}/logs/{logId}", habitHandler.DeleteLog)
|
|
|
|
// Habit freezes
|
|
r.Get("/habits/{id}/freezes", habitFreezeHandler.List)
|
|
r.Post("/habits/{id}/freezes", habitFreezeHandler.Create)
|
|
r.Delete("/habits/{id}/freezes/{freezeId}", habitFreezeHandler.Delete)
|
|
|
|
// Stats
|
|
r.Get("/habits/stats", habitHandler.Stats)
|
|
r.Get("/habits/{id}/stats", habitHandler.HabitStats)
|
|
|
|
// Tasks routes
|
|
r.Get("/tasks", taskHandler.List)
|
|
r.Get("/tasks/today", taskHandler.Today)
|
|
r.Post("/tasks", taskHandler.Create)
|
|
r.Get("/tasks/{id}", taskHandler.Get)
|
|
r.Put("/tasks/{id}", taskHandler.Update)
|
|
r.Delete("/tasks/{id}", taskHandler.Delete)
|
|
r.Post("/tasks/{id}/complete", taskHandler.Complete)
|
|
r.Post("/tasks/{id}/uncomplete", taskHandler.Uncomplete)
|
|
|
|
// Savings routes
|
|
r.Get("/savings/categories", savingsHandler.ListCategories)
|
|
r.Post("/savings/categories", savingsHandler.CreateCategory)
|
|
r.Get("/savings/categories/{id}", savingsHandler.GetCategory)
|
|
r.Put("/savings/categories/{id}", savingsHandler.UpdateCategory)
|
|
r.Delete("/savings/categories/{id}", savingsHandler.DeleteCategory)
|
|
|
|
// Savings category members
|
|
r.Get("/savings/categories/{id}/members", savingsHandler.ListMembers)
|
|
r.Post("/savings/categories/{id}/members", savingsHandler.AddMember)
|
|
r.Delete("/savings/categories/{id}/members/{userId}", savingsHandler.RemoveMember)
|
|
|
|
// Savings recurring plans
|
|
r.Get("/savings/categories/{id}/recurring-plans", savingsHandler.ListRecurringPlans)
|
|
r.Post("/savings/categories/{id}/recurring-plans", savingsHandler.CreateRecurringPlan)
|
|
r.Delete("/savings/recurring-plans/{planId}", savingsHandler.DeleteRecurringPlan)
|
|
r.Put("/savings/recurring-plans/{planId}", savingsHandler.UpdateRecurringPlan)
|
|
|
|
// Savings transactions
|
|
r.Get("/savings/transactions", savingsHandler.ListTransactions)
|
|
r.Post("/savings/transactions", savingsHandler.CreateTransaction)
|
|
r.Get("/savings/transactions/{id}", savingsHandler.GetTransaction)
|
|
r.Put("/savings/transactions/{id}", savingsHandler.UpdateTransaction)
|
|
r.Delete("/savings/transactions/{id}", savingsHandler.DeleteTransaction)
|
|
|
|
// Savings stats
|
|
r.Get("/savings/stats", savingsHandler.Stats)
|
|
|
|
// Finance routes (owner-only, checked in handler)
|
|
r.Get("/finance/categories", financeHandler.ListCategories)
|
|
r.Post("/finance/categories", financeHandler.CreateCategory)
|
|
r.Put("/finance/categories/{id}", financeHandler.UpdateCategory)
|
|
r.Delete("/finance/categories/{id}", financeHandler.DeleteCategory)
|
|
|
|
r.Get("/finance/transactions", financeHandler.ListTransactions)
|
|
r.Post("/finance/transactions", financeHandler.CreateTransaction)
|
|
r.Put("/finance/transactions/{id}", financeHandler.UpdateTransaction)
|
|
r.Delete("/finance/transactions/{id}", financeHandler.DeleteTransaction)
|
|
|
|
r.Get("/finance/summary", financeHandler.Summary)
|
|
r.Get("/finance/analytics", financeHandler.Analytics)
|
|
})
|
|
|
|
port := os.Getenv("PORT")
|
|
if port == "" {
|
|
port = "8080"
|
|
}
|
|
|
|
log.Printf("Server starting on :%s", port)
|
|
if err := http.ListenAndServe(":"+port, r); err != nil {
|
|
log.Fatalf("Server failed: %v", err)
|
|
}
|
|
}
|