package main import ( "context" "encoding/json" "errors" "fmt" "log/slog" "net/http" "os" "time" "card-to-iban/internal/httpapi" "card-to-iban/internal/repository" "card-to-iban/internal/service" "card-to-iban/internal/storage" "card-to-iban/internal/zarinhub" "github.com/joho/godotenv" ) func main() { logger := slog.New( slog.NewJSONHandler(os.Stdout, nil), ) if err := run(logger); err != nil { logger.Error("server stopped", "error", err) os.Exit(1) } } func run(logger *slog.Logger) error { loadLocalEnvironment() config, err := loadConfig() if err != nil { return fmt.Errorf("load configuration: %w", err) } httpClient := &http.Client{Timeout: config.ZarinHubTimeout} tokenManager := zarinhub.NewTokenManager( httpClient, config.ZarinHubAuthEndpoint, config.ZarinHubUsername, config.ZarinHubPassword, ) authContext, cancelAuth := context.WithTimeout( context.Background(), config.ZarinHubTimeout, ) _, err = tokenManager.AccessToken(authContext) cancelAuth() if err != nil { return fmt.Errorf("authenticate with zarinhub: %w", err) } db, err := storage.OpenSQLite(config.DatabasePath) if err != nil { return err } migrationContext, cancelMigration := context.WithTimeout( context.Background(), 5*time.Second, ) defer cancelMigration() if err := storage.Migrate(migrationContext, db); err != nil { if closeErr := db.Close(); closeErr != nil { return errors.Join(err, closeErr) } return err } zarinHubClient := zarinhub.NewClient( httpClient, config.ZarinHubEndpoint, tokenManager, ) cardService := service.NewCardService(zarinHubClient) auditRepository := repository.NewAuditRepository(db) handler := httpapi.NewHandler( cardService, auditRepository, logger, ) mux := http.NewServeMux() mux.HandleFunc("/health", healthHandler) mux.HandleFunc("/api/v1/card-to-iban", handler.Convert) httpapi.RegisterDocumentation(mux) server := &http.Server{ Addr: config.ServerAddr, Handler: httpapi.AllowOrigin( config.FrontendOrigin, mux, ), ReadHeaderTimeout: 5 * time.Second, ReadTimeout: 10 * time.Second, WriteTimeout: 15 * time.Second, IdleTimeout: 60 * time.Second, } logger.Info("server listening", "address", config.ServerAddr) serverErr := server.ListenAndServe() closeErr := db.Close() return errors.Join(serverErr, closeErr) } func loadLocalEnvironment() { for _, path := range []string{".env", "../.env"} { if err := godotenv.Load(path); err == nil { return } } } func healthHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } w.Header().Set("Content-Type", "application/json") response := map[string]string{ "status": "ok", } if err := json.NewEncoder(w).Encode(response); err != nil { slog.ErrorContext( r.Context(), "health response encoding failed", "error", err, ) } }