mirror of
https://github.com/Sosokker/go-chi-oapi-codegen-todolist.git
synced 2025-12-19 14:04:07 +01:00
246 lines
6.8 KiB
Go
246 lines
6.8 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"log/slog"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/Sosokker/todolist-backend/internal/api"
|
|
"github.com/Sosokker/todolist-backend/internal/cache"
|
|
"github.com/Sosokker/todolist-backend/internal/config"
|
|
"github.com/Sosokker/todolist-backend/internal/repository"
|
|
"github.com/Sosokker/todolist-backend/internal/service"
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/go-chi/chi/v5/middleware"
|
|
"github.com/go-chi/cors"
|
|
"github.com/golang-migrate/migrate/v4"
|
|
_ "github.com/golang-migrate/migrate/v4/database/postgres"
|
|
_ "github.com/golang-migrate/migrate/v4/source/file"
|
|
)
|
|
|
|
func main() {
|
|
configPath := flag.String("config", ".", "Path to the config directory or file")
|
|
flag.Parse()
|
|
|
|
cfg, err := config.LoadConfig(*configPath)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error loading configuration: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
logger := setupLogger(cfg.Log)
|
|
slog.SetDefault(logger)
|
|
|
|
logger.Info("Starting Todolist Backend Service", "version", "1.2.0")
|
|
logger.Debug("Configuration loaded", "config", cfg)
|
|
|
|
pool, err := repository.NewConnectionPool(cfg.Database)
|
|
if err != nil {
|
|
logger.Error("Failed to connect to database", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
defer pool.Close()
|
|
|
|
if err := runMigrations(cfg.Database.URL); err != nil {
|
|
logger.Error("Database migration failed", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// --- Cache Setup ---
|
|
appCache := cache.NewMemoryCache(cfg.Cache, logger)
|
|
|
|
repoRegistry := repository.NewRepositoryRegistry(pool, appCache, logger)
|
|
|
|
var storageService service.FileStorageService
|
|
storageService, err = service.NewGCStorageService(cfg.Storage.GCS, logger)
|
|
if err != nil {
|
|
logger.Error("Failed to initialize storage service", "error", err, "type", cfg.Storage.Type)
|
|
os.Exit(1)
|
|
}
|
|
|
|
authService := service.NewAuthService(repoRegistry.UserRepo, cfg)
|
|
userService := service.NewUserService(repoRegistry.UserRepo)
|
|
tagService := service.NewTagService(repoRegistry.TagRepo)
|
|
subtaskService := service.NewSubtaskService(repoRegistry.SubtaskRepo)
|
|
todoService := service.NewTodoService(repoRegistry.TodoRepo, tagService, subtaskService, storageService)
|
|
|
|
services := &service.ServiceRegistry{
|
|
Auth: authService,
|
|
User: userService,
|
|
Tag: tagService,
|
|
Todo: todoService,
|
|
Subtask: subtaskService,
|
|
Storage: storageService,
|
|
}
|
|
|
|
apiHandler := api.NewApiHandler(services, cfg, logger)
|
|
|
|
r := chi.NewRouter()
|
|
|
|
r.Use(middleware.Logger)
|
|
r.Use(middleware.RequestID)
|
|
r.Use(middleware.RealIP)
|
|
r.Use(NewStructuredLogger(logger))
|
|
r.Use(middleware.Recoverer)
|
|
r.Use(cors.Handler(cors.Options{
|
|
AllowedOrigins: []string{"http://localhost:3000", "https://your-frontend-domain.com"},
|
|
AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
|
|
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
|
|
ExposedHeaders: []string{"Link"},
|
|
AllowCredentials: true,
|
|
MaxAge: 300,
|
|
}))
|
|
r.Use(middleware.Timeout(60 * time.Second))
|
|
|
|
r.Route(cfg.Server.BasePath, func(subr chi.Router) {
|
|
subr.Post("/auth/signup", apiHandler.SignupUserApi)
|
|
subr.Post("/auth/login", apiHandler.LoginUserApi)
|
|
subr.Get("/auth/google/login", apiHandler.InitiateGoogleLogin)
|
|
subr.Get("/auth/google/callback", apiHandler.HandleGoogleCallback)
|
|
|
|
subr.Group(func(prot chi.Router) {
|
|
prot.Use(api.AuthMiddleware(authService, cfg))
|
|
api.HandlerFromMux(apiHandler, prot)
|
|
})
|
|
})
|
|
|
|
r.Get("/health", func(w http.ResponseWriter, r *http.Request) {
|
|
if err := pool.Ping(r.Context()); err != nil {
|
|
http.Error(w, "Health check failed: DB ping error", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
fmt.Fprintln(w, "OK")
|
|
})
|
|
|
|
srv := &http.Server{
|
|
Addr: fmt.Sprintf(":%d", cfg.Server.Port),
|
|
Handler: r,
|
|
ReadTimeout: cfg.Server.ReadTimeout,
|
|
WriteTimeout: cfg.Server.WriteTimeout,
|
|
IdleTimeout: cfg.Server.IdleTimeout,
|
|
}
|
|
|
|
go func() {
|
|
logger.Info("Server starting", "address", srv.Addr)
|
|
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
logger.Error("Server failed to start", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
}()
|
|
|
|
quit := make(chan os.Signal, 1)
|
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
|
<-quit
|
|
logger.Info("Shutting down server...")
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
if err := srv.Shutdown(ctx); err != nil {
|
|
logger.Error("Server shutdown failed", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
logger.Info("Server exited gracefully")
|
|
}
|
|
|
|
func setupLogger(cfg config.LogConfig) *slog.Logger {
|
|
var level slog.Level
|
|
switch strings.ToLower(cfg.Level) {
|
|
case "debug":
|
|
level = slog.LevelDebug
|
|
case "info":
|
|
level = slog.LevelInfo
|
|
case "warn":
|
|
level = slog.LevelWarn
|
|
case "error":
|
|
level = slog.LevelError
|
|
default:
|
|
level = slog.LevelInfo
|
|
}
|
|
|
|
opts := &slog.HandlerOptions{
|
|
Level: level,
|
|
AddSource: level == slog.LevelDebug,
|
|
}
|
|
|
|
var handler slog.Handler
|
|
if cfg.Format == "json" {
|
|
handler = slog.NewJSONHandler(os.Stdout, opts)
|
|
} else {
|
|
handler = slog.NewTextHandler(os.Stdout, opts)
|
|
}
|
|
|
|
return slog.New(handler)
|
|
}
|
|
|
|
func runMigrations(databaseURL string) error {
|
|
if databaseURL == "" {
|
|
return errors.New("database URL is required for migrations")
|
|
}
|
|
migrationPath := "file://migrations"
|
|
|
|
m, err := migrate.New(migrationPath, databaseURL)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create migrate instance: %w", err)
|
|
}
|
|
|
|
err = m.Up()
|
|
if err != nil && !errors.Is(err, migrate.ErrNoChange) {
|
|
sourceErr, dbErr := m.Close()
|
|
slog.Error("Migration close errors", "source_error", sourceErr, "db_error", dbErr)
|
|
return fmt.Errorf("failed to apply migrations: %w", err)
|
|
}
|
|
|
|
if errors.Is(err, migrate.ErrNoChange) {
|
|
slog.Info("No new migrations to apply")
|
|
} else {
|
|
slog.Info("Database migrations applied successfully")
|
|
}
|
|
|
|
sourceErr, dbErr := m.Close()
|
|
if sourceErr != nil {
|
|
slog.Error("Error closing migration source", "error", sourceErr)
|
|
}
|
|
if dbErr != nil {
|
|
slog.Error("Error closing migration database connection", "error", dbErr)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func NewStructuredLogger(logger *slog.Logger) func(next http.Handler) http.Handler {
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor)
|
|
start := time.Now()
|
|
reqLogger := logger.With(
|
|
slog.String("proto", r.Proto),
|
|
slog.String("method", r.Method),
|
|
slog.String("path", r.URL.Path),
|
|
slog.String("remote_addr", r.RemoteAddr),
|
|
slog.String("user_agent", r.UserAgent()),
|
|
slog.String("request_id", middleware.GetReqID(r.Context())),
|
|
)
|
|
|
|
defer func() {
|
|
reqLogger.Info("request completed",
|
|
slog.Int("status", ww.Status()),
|
|
slog.Duration("latency", time.Since(start)),
|
|
)
|
|
}()
|
|
|
|
next.ServeHTTP(ww, r)
|
|
})
|
|
}
|
|
}
|